├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── art ├── hippo-elephant.gif └── screencap.gif ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kyrie ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── github │ └── alexjlockwood │ └── kyrie │ ├── Animation.kt │ ├── CircleNode.kt │ ├── ClipPathNode.kt │ ├── ClipType.kt │ ├── ComplexColor.java │ ├── EllipseNode.kt │ ├── Extensions.kt │ ├── FillType.kt │ ├── GradientColorInflater.java │ ├── GroupNode.kt │ ├── InflationUtils.java │ ├── Keyframe.kt │ ├── KeyframeSet.kt │ ├── KyrieDrawable.kt │ ├── LineNode.kt │ ├── Node.kt │ ├── ObjectKeyframeSet.kt │ ├── PathData.kt │ ├── PathDataUtils.kt │ ├── PathKeyframeSet.kt │ ├── PathNode.kt │ ├── Property.kt │ ├── PropertyTimeline.kt │ ├── RectangleNode.kt │ ├── RenderNode.kt │ ├── StrokeLineCap.kt │ ├── StrokeLineJoin.kt │ ├── Styleable.java │ ├── TransformNode.kt │ ├── TypedArrayUtils.kt │ └── package-info.java ├── sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── example │ │ └── kyrie │ │ ├── DemoListFragment.kt │ │ ├── HeartbreakFragment.kt │ │ ├── MainActivity.kt │ │ ├── PathMorphFragment.java │ │ ├── PolygonsFragment.java │ │ ├── ProgressFragment.kt │ │ ├── SampleListenerAdapter.kt │ │ ├── SampleOnClickListener.kt │ │ └── SampleOnSeekBarChangeListener.kt │ └── res │ ├── drawable │ ├── avd_heartbreak.xml │ └── ic_launcher_foreground.xml │ ├── layout-land │ └── fragment_two_pane.xml │ ├── layout │ ├── activity_main.xml │ ├── fragment_demo_item.xml │ ├── fragment_demo_list.xml │ ├── fragment_seekbar.xml │ └── fragment_two_pane.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── heartbreak.xml │ ├── path_morphing.xml │ ├── strings.xml │ └── styles.xml ├── scripts ├── README.md └── publish.gradle └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | #Android generated 2 | bin 3 | gen 4 | 5 | #Eclipse 6 | .project 7 | .classpath 8 | .settings 9 | 10 | #IntelliJ IDEA 11 | .idea 12 | *.iml 13 | *.ipr 14 | *.iws 15 | out 16 | 17 | #Maven 18 | target 19 | release.properties 20 | pom.xml.* 21 | 22 | #Ant 23 | build.xml 24 | local.properties 25 | proguard.cfg 26 | 27 | #OSX 28 | .DS_Store 29 | 30 | #Gradle 31 | .gradle 32 | build 33 | 34 | #Emacs 35 | *~ 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | android: 7 | components: 8 | - tools 9 | - platform-tools 10 | - build-tools-27.0.2 11 | - android-27 12 | - extra-android-support 13 | licenses: 14 | - android-sdk-license-.+ 15 | 16 | before_install: 17 | - yes | sdkmanager "platforms;android-27" 18 | - yes | sdkmanager "sources;android-27" 19 | - yes | sdkmanager "docs" 20 | 21 | after_success: 22 | - ./gradlew dokka 23 | - mv kyrie/build/javadoc/style.css kyrie/build/javadoc/kyrie 24 | - for f in `find kyrie/build/javadoc/kyrie/ -name "*.html"`; do sed -i 's/..\/style.css/style.css/g' $f; done 25 | 26 | deploy: 27 | provider: pages 28 | skip-cleanup: true 29 | github-token: $GITHUB_TOKEN 30 | keep-history: true 31 | local-dir: kyrie/build/javadoc/kyrie 32 | on: 33 | branch: master 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kyrie 2 | 3 | [![Build status][travis-badge]][travis-badge-url] 4 | [![Download](https://api.bintray.com/packages/alexjlockwood/maven/kyrie/images/download.svg)](https://bintray.com/alexjlockwood/maven/kyrie/_latestVersion) 5 | 6 | Kyrie is a superset of Android's `VectorDrawable` and `AnimatedVectorDrawable` classes: it can do everything they can do and more. 7 | 8 | ![Screen capture of tool](art/screencap.gif) 9 | 10 | ## Motivation 11 | 12 | `VectorDrawable`s are great because they provide density independence—they can be scaled arbitrarily on any device without loss of quality. `AnimatedVectorDrawable`s make them even more awesome, allowing us to animate specific properties of a `VectorDrawable` in a variety of ways. 13 | 14 | However, these two classes have three main limitations: 15 | 16 | 1. They can't be paused, resumed, or seeked. 17 | 2. They can't be dynamically created at runtime (they must be inflated from a drawable resource). 18 | 3. They only support a small subset of features that SVGs provide on the web. 19 | 20 | Kyrie was created in order to address these problems. 21 | 22 | ## Getting started 23 | 24 | To create an animation using Kyrie, you first need to build a [`KyrieDrawable`][kyriedrawable]. There are two ways to do this: 25 | 26 | ### Option #1: from an existing VD/AVD resource 27 | 28 | With Kyrie, you can convert an existing `VectorDrawable` or `AnimatedVectorDrawable` resource into a `KyrieDrawable` with a single line: 29 | 30 | ```kotlin 31 | val drawable = KyrieDrawable.create(context, R.drawable.my_vd_or_avd); 32 | ``` 33 | 34 | ### Option #2: programatically using a [`KyrieDrawable.Builder`][kyriedrawable#builder] 35 | 36 | You can also build `KyrieDrawable`s at runtime using the builder pattern. `KyrieDrawable`s are similar to SVGs and `VectorDrawable`s in that they are tree-like structures built of [`Node`][node]s. As you build the tree, you can optionally assign [`Animation`][animation]s to the properties of each `Node` to create an animatable `KyrieDrawable`. 37 | 38 | Here is a snippet of code from the [sample app][sample-app-source-code] that builds a material design circular progress indicator: 39 | 40 | ```kotlin 41 | val drawable = 42 | kyrieDrawable { 43 | viewport = size(48f, 48f) 44 | tint = Color.RED 45 | group { 46 | translateX(24f) 47 | translateY(24f) 48 | rotation( 49 | Animation.ofFloat(0f, 720f) 50 | .duration(4444) 51 | .repeatCount(Animation.INFINITE) 52 | ) 53 | path { 54 | strokeColor(Color.WHITE) 55 | strokeWidth(4f) 56 | trimPathStart( 57 | Animation.ofFloat(0f, 0.75f) 58 | .duration(1333) 59 | .repeatCount(Animation.INFINITE) 60 | .interpolator("M 0 0 h .5 C .7 0 .6 1 1 1".asPathInterpolator()) 61 | ) 62 | trimPathEnd( 63 | Animation.ofFloat(0.03f, 0.78f) 64 | .duration(1333) 65 | .repeatCount(Animation.INFINITE) 66 | .interpolator("M 0 0 c .2 0 .1 1 .5 1 C 1 1 1 1 1 1".asPathInterpolator()) 67 | ) 68 | trimPathOffset( 69 | Animation.ofFloat(0f, 0.25f) 70 | .duration(1333) 71 | .repeatCount(Animation.INFINITE) 72 | ) 73 | strokeLineCap(StrokeLineCap.SQUARE) 74 | pathData("M 0 -18 a 18 18 0 1 1 0 36 18 18 0 1 1 0 -36") 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | ## Features 81 | 82 | Kyrie supports 100% of the features that `VectorDrawable`s and `AnimatedVectorDrawable`s provide. It also extends the functionality of `VectorDrawable`s and `AnimatedVectorDrawable`s in a number of ways, making it possible to create even more powerful and elaborate scalable assets and animations. 83 | 84 | ### `VectorDrawable` features 85 | 86 | In addition to the features supported by `VectorDrawable`, Kyrie provides the following: 87 | 88 | #### `` features 89 | 90 | - `CircleNode`. Equivalent to the `` node in SVG. 91 | - `EllipseNode`. Equivalent to the `` node in SVG. 92 | - `LineNode`. Equivalent to the `` node in SVG. 93 | - `RectangleNode`. Equivalent to the `` node in SVG. 94 | - `strokeDashArray` (`FloatArray`). Equivalent to the `stroke-dasharray` attribute in SVG. 95 | - `strokeDashOffset` (`Float`). Equivalent to the `stroke-dashoffset` attribute in SVG. 96 | - `isScalingStroke` (`Boolean`). Equivalent to `vector-effect="non-scaling-stroke"` in SVG. Defines whether a path's stroke width will be affected by scaling transformations. 97 | - The `strokeMiterLimit` attribute is animatable. 98 | 99 | #### `` features 100 | 101 | - `FillType` (either `NON_ZERO` or `EVEN_ODD`). Equivalent to the `clip-rule` attribute in SVG. 102 | - `ClipType` (either `INTERSECT` or `DIFFERENCE`). Defines whether the clipping region is additive or subtractive. 103 | 104 | #### `` features 105 | 106 | - Transformations (`pivot`, `scale`, `rotation`, and `translation`) can be set on _any_ `Node`, not just `GroupNode`s. 107 | 108 | ### `AnimatedVectorDrawable` features 109 | 110 | In addition to the features supported by `AnimatedVectorDrawable`, Kyrie provides the following: 111 | 112 | - [`setCurrentPlayTime(long)`][kyriedrawable#setcurrentplaytime]. 113 | - Allows you to manually scrub the animation. 114 | - [`pause()`][kyriedrawable#pause] and [`resume()`][kyriedrawable#resume]. 115 | - Allows you to pause and resume the animation. 116 | - [`addListener(KyrieDrawable.Listener)`][kyriedrawable#addlistener]. 117 | - Allows you to listen for the following animation events: start, update, pause, resume, cancel, and end. 118 | 119 | ## Further reading 120 | 121 | - Check out [this blog post][adp-blog-post] for more on the motivation behind the library. 122 | - Check out [the sample app][sample-app-source-code] for example usages in both Java and Kotlin. 123 | - Check out [the documentation][documentation] for a complete listing of all supported `Animation`s and `Node`s that can be used when constructing `KyrieDrawable`s programatically. 124 | 125 | ## Dependency 126 | 127 | Add this to your root `build.gradle` file (_not_ your module's `build.gradle` file): 128 | 129 | ```gradle 130 | allprojects { 131 | repositories { 132 | // ... 133 | jcenter() 134 | } 135 | } 136 | ``` 137 | 138 | Then add the library to your module's `build.gradle` file: 139 | 140 | ```gradle 141 | dependencies { 142 | // ... 143 | implementation 'com.github.alexjlockwood:kyrie:0.2.1' 144 | } 145 | ``` 146 | 147 | ## Compatibility 148 | 149 | - **Minimum Android SDK**: Kyrie requires a minimum API level of 14. 150 | - **Compile Android SDK**: Kyrie requires you to compile against API 28 or later. 151 | 152 | [travis-badge]: https://travis-ci.org/alexjlockwood/kyrie.svg?branch=master 153 | [travis-badge-url]: https://travis-ci.org/alexjlockwood/kyrie 154 | [kyriedrawable]:https://alexjlockwood.github.io/kyrie/com.github.alexjlockwood.kyrie/-kyrie-drawable/index.html 155 | [node]: https://alexjlockwood.github.io/kyrie/com.github.alexjlockwood.kyrie/-node/index.html 156 | [animation]: https://alexjlockwood.github.io/kyrie/com.github.alexjlockwood.kyrie/-animation/index.html 157 | [progressfragment]: https://github.com/alexjlockwood/kyrie/blob/master/sample/src/main/java/com/example/kyrie/ProgressFragment.kt 158 | [kyriedrawable#setcurrentplaytime]: https://alexjlockwood.github.io/kyrie/com.github.alexjlockwood.kyrie/-kyrie-drawable/current-play-time.html 159 | [kyriedrawable#pause]: https://alexjlockwood.github.io/kyrie/com.github.alexjlockwood.kyrie/-kyrie-drawable/pause.html 160 | [kyriedrawable#resume]: https://alexjlockwood.github.io/kyrie/com.github.alexjlockwood.kyrie/-kyrie-drawable/resume.html 161 | [kyriedrawable#addlistener]: https://alexjlockwood.github.io/kyrie/com.github.alexjlockwood.kyrie/-kyrie-drawable/add-listener.html 162 | [kyriedrawable#builder]: https://alexjlockwood.github.io/kyrie/com.github.alexjlockwood.kyrie/-kyrie-drawable/-builder/index.html 163 | [documentation]: https://alexjlockwood.github.io/kyrie/com.github.alexjlockwood.kyrie/index.html 164 | [sample-app-source-code]: https://github.com/alexjlockwood/kyrie/tree/master/sample/src/main/java/com/example/kyrie 165 | [adp-blog-post]: https://www.androiddesignpatterns.com/2018/03/introducing-kyrie-animated-vector-drawables.html 166 | -------------------------------------------------------------------------------- /art/hippo-elephant.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/art/hippo-elephant.gif -------------------------------------------------------------------------------- /art/screencap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/art/screencap.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlinVersion = '1.3.31' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.4.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 11 | classpath 'org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18' 12 | classpath 'com.novoda:bintray-release:0.9.1' 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | } 22 | 23 | ext { 24 | minSdkVersion = 14 25 | targetSdkVersion = 28 26 | } 27 | -------------------------------------------------------------------------------- /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 | android.enableJetifier=true 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun May 12 12:11:07 PDT 2019 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-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --pathData --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --pathData --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 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 --pathData --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 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /kyrie/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'org.jetbrains.dokka-android' 4 | 5 | android { 6 | compileSdkVersion rootProject.ext.targetSdkVersion 7 | 8 | defaultConfig { 9 | minSdkVersion rootProject.ext.minSdkVersion 10 | targetSdkVersion rootProject.ext.targetSdkVersion 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | dokka { 16 | outputFormat = 'html' 17 | outputDirectory = "$buildDir/javadoc" 18 | } 19 | } 20 | 21 | dependencies { 22 | api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 23 | // TODO: depend on appcompat-resources v1.1.0 once it graduates from alpha 24 | implementation 'androidx.appcompat:appcompat:1.0.2' 25 | compileOnly 'com.google.code.findbugs:jsr305:3.0.2' 26 | } 27 | 28 | ext { 29 | bintrayRepo = 'maven' 30 | bintrayName = 'kyrie' 31 | 32 | publishedGroupId = 'com.github.alexjlockwood' 33 | libraryName = 'Kyrie' 34 | artifact = 'kyrie' 35 | 36 | libraryDescription = 'Animated Vector Drawables on steroids' 37 | 38 | siteUrl = 'https://github.com/alexjlockwood/kyrie' 39 | gitUrl = 'https://github.com/alexjlockwood/kyrie.git' 40 | 41 | libraryVersion = '0.2.1' 42 | 43 | developerId = 'alexjlockwood' 44 | developerName = 'Alex Lockwood' 45 | developerEmail = 'alexjlockwood@gmail.com' 46 | 47 | licenseName = 'The Apache Software License, Version 2.0' 48 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 49 | allLicenses = ["Apache-2.0"] 50 | } 51 | 52 | if (project.rootProject.file('local.properties').exists()) { 53 | apply from: '../scripts/publish.gradle' 54 | } 55 | -------------------------------------------------------------------------------- /kyrie/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/CircleNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Path 4 | import android.graphics.RectF 5 | import androidx.annotation.FloatRange 6 | 7 | /** A [Node] that paints a circle. */ 8 | class CircleNode private constructor( 9 | rotation: List>, 10 | pivotX: List>, 11 | pivotY: List>, 12 | scaleX: List>, 13 | scaleY: List>, 14 | translateX: List>, 15 | translateY: List>, 16 | fillColor: List>, 17 | fillColorComplex: ComplexColor?, 18 | fillAlpha: List>, 19 | strokeColor: List>, 20 | strokeColorComplex: ComplexColor?, 21 | strokeAlpha: List>, 22 | strokeWidth: List>, 23 | trimPathStart: List>, 24 | trimPathEnd: List>, 25 | trimPathOffset: List>, 26 | strokeLineCap: StrokeLineCap, 27 | strokeLineJoin: StrokeLineJoin, 28 | strokeMiterLimit: List>, 29 | strokeDashArray: List>, 30 | strokeDashOffset: List>, 31 | fillType: FillType, 32 | isStrokeScaling: Boolean, 33 | private val centerX: List>, 34 | private val centerY: List>, 35 | private val radius: List> 36 | ) : RenderNode( 37 | rotation, 38 | pivotX, 39 | pivotY, 40 | scaleX, 41 | scaleY, 42 | translateX, 43 | translateY, 44 | fillColor, 45 | fillColorComplex, 46 | fillAlpha, 47 | strokeColor, 48 | strokeColorComplex, 49 | strokeAlpha, 50 | strokeWidth, 51 | trimPathStart, 52 | trimPathEnd, 53 | trimPathOffset, 54 | strokeLineCap, 55 | strokeLineJoin, 56 | strokeMiterLimit, 57 | strokeDashArray, 58 | strokeDashOffset, 59 | fillType, 60 | isStrokeScaling 61 | ) { 62 | 63 | // 64 | 65 | override fun toLayer(timeline: PropertyTimeline): CircleLayer { 66 | return CircleLayer(timeline, this) 67 | } 68 | 69 | internal class CircleLayer(timeline: PropertyTimeline, node: CircleNode) : RenderNode.RenderLayer(timeline, node) { 70 | private val centerX = registerAnimatableProperty(node.centerX) 71 | private val centerY = registerAnimatableProperty(node.centerY) 72 | private val radius = registerAnimatableProperty(node.radius) 73 | 74 | private val tempRect = RectF() 75 | 76 | override fun onInitPath(outPath: Path) { 77 | val cx = centerX.animatedValue 78 | val cy = centerY.animatedValue 79 | val r = radius.animatedValue 80 | tempRect.set(cx - r, cy - r, cx + r, cy + r) 81 | outPath.addOval(tempRect, Path.Direction.CW) 82 | } 83 | } 84 | 85 | // 86 | 87 | // 88 | 89 | @DslMarker 90 | private annotation class CircleNodeMarker 91 | 92 | /** Builder class used to create [CircleNode]s. */ 93 | @CircleNodeMarker 94 | class Builder internal constructor() : RenderNode.Builder() { 95 | private val centerX = asAnimations(0f) 96 | private val centerY = asAnimations(0f) 97 | private val radius = asAnimations(0f) 98 | 99 | // Center X. 100 | 101 | fun centerX(initialCenterX: Float): Builder { 102 | return replaceFirstAnimation(centerX, asAnimation(initialCenterX)) 103 | } 104 | 105 | @SafeVarargs 106 | fun centerX(vararg animations: Animation<*, Float>): Builder { 107 | return replaceAnimations(centerX, *animations) 108 | } 109 | 110 | fun centerX(animations: List>): Builder { 111 | return replaceAnimations(centerX, animations) 112 | } 113 | 114 | // Center Y. 115 | 116 | fun centerY(initialCenterY: Float): Builder { 117 | return replaceFirstAnimation(centerY, asAnimation(initialCenterY)) 118 | } 119 | 120 | @SafeVarargs 121 | fun centerY(vararg animations: Animation<*, Float>): Builder { 122 | return replaceAnimations(centerY, *animations) 123 | } 124 | 125 | fun centerY(animations: List>): Builder { 126 | return replaceAnimations(centerY, animations) 127 | } 128 | 129 | // Radius. 130 | 131 | fun radius(@FloatRange(from = 0.0) initialRadius: Float): Builder { 132 | return replaceFirstAnimation(radius, asAnimation(initialRadius)) 133 | } 134 | 135 | @SafeVarargs 136 | fun radius(vararg animations: Animation<*, Float>): Builder { 137 | return replaceAnimations(radius, *animations) 138 | } 139 | 140 | fun radius(animations: List>): Builder { 141 | return replaceAnimations(radius, animations) 142 | } 143 | 144 | override val self = this 145 | 146 | override fun build(): CircleNode { 147 | return CircleNode( 148 | rotation, 149 | pivotX, 150 | pivotY, 151 | scaleX, 152 | scaleY, 153 | translateX, 154 | translateY, 155 | fillColor, 156 | fillColorComplex, 157 | fillAlpha, 158 | strokeColor, 159 | strokeColorComplex, 160 | strokeAlpha, 161 | strokeWidth, 162 | trimPathStart, 163 | trimPathEnd, 164 | trimPathOffset, 165 | strokeLineCap, 166 | strokeLineJoin, 167 | strokeMiterLimit, 168 | strokeDashArray, 169 | strokeDashOffset, 170 | fillType, 171 | isScalingStroke, 172 | centerX, 173 | centerY, 174 | radius 175 | ) 176 | } 177 | } 178 | 179 | // 180 | 181 | companion object { 182 | 183 | @JvmStatic 184 | fun builder(): Builder { 185 | return Builder() 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/ClipPathNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Matrix 5 | import android.graphics.Path 6 | import android.graphics.PointF 7 | import android.graphics.Region 8 | 9 | /** 10 | * A [Node] that defines a region to be clipped. Note that a [ClipPathNode] only clips 11 | * its sibling [Node]s. 12 | */ 13 | class ClipPathNode private constructor( 14 | rotation: List>, 15 | pivotX: List>, 16 | pivotY: List>, 17 | scaleX: List>, 18 | scaleY: List>, 19 | translateX: List>, 20 | translateY: List>, 21 | private val pathData: List>, 22 | private val fillType: FillType, 23 | private val clipType: ClipType 24 | ) : TransformNode(rotation, pivotX, pivotY, scaleX, scaleY, translateX, translateY) { 25 | 26 | // 27 | 28 | override fun toLayer(timeline: PropertyTimeline): ClipPathLayer { 29 | return ClipPathLayer(timeline, this) 30 | } 31 | 32 | internal class ClipPathLayer(timeline: PropertyTimeline, node: ClipPathNode) : TransformNode.TransformLayer(timeline, node) { 33 | private val pathData = registerAnimatableProperty(node.pathData) 34 | private val fillType = node.fillType 35 | private val clipType = node.clipType 36 | 37 | private val tempMatrix = Matrix() 38 | private val tempPath = Path() 39 | private val tempRenderPath = Path() 40 | 41 | override fun onDraw(canvas: Canvas, parentMatrix: Matrix, viewportScale: PointF) { 42 | val matrixScale = getMatrixScale(parentMatrix) 43 | if (matrixScale == 0f) { 44 | return 45 | } 46 | 47 | val scaleX = viewportScale.x 48 | val scaleY = viewportScale.y 49 | tempMatrix.set(parentMatrix) 50 | if (scaleX != 1f || scaleY != 1f) { 51 | tempMatrix.postScale(scaleX, scaleY) 52 | } 53 | 54 | tempRenderPath.reset() 55 | tempPath.reset() 56 | PathData.toPath(pathData.animatedValue, tempPath) 57 | tempRenderPath.addPath(tempPath, tempMatrix) 58 | tempRenderPath.fillType = getPaintFillType(fillType) 59 | if (clipType == ClipType.INTERSECT) { 60 | canvas.clipPath(tempRenderPath) 61 | } else { 62 | canvas.clipPath(tempRenderPath, Region.Op.DIFFERENCE) 63 | } 64 | } 65 | 66 | private fun getPaintFillType(fillType: FillType): Path.FillType { 67 | return when (fillType) { 68 | FillType.NON_ZERO -> Path.FillType.WINDING 69 | FillType.EVEN_ODD -> Path.FillType.EVEN_ODD 70 | } 71 | } 72 | 73 | override fun isStateful(): Boolean { 74 | return false 75 | } 76 | 77 | override fun onStateChange(stateSet: IntArray): Boolean { 78 | return false 79 | } 80 | } 81 | 82 | // 83 | 84 | // 85 | 86 | @DslMarker 87 | private annotation class ClipPathNodeMarker 88 | 89 | /** Builder class used to create [ClipPathNode]s. */ 90 | @ClipPathNodeMarker 91 | class Builder internal constructor() : TransformNode.Builder() { 92 | private val pathData = asAnimations(PathData()) 93 | private var fillType = FillType.NON_ZERO 94 | private var clipType = ClipType.INTERSECT 95 | 96 | // Path data. 97 | 98 | fun pathData(initialPathData: String): Builder { 99 | return pathData(PathData.parse(initialPathData)) 100 | } 101 | 102 | fun pathData(initialPathData: PathData): Builder { 103 | return replaceFirstAnimation(pathData, asAnimation(initialPathData)) 104 | } 105 | 106 | @SafeVarargs 107 | fun pathData(vararg animations: Animation<*, PathData>): Builder { 108 | return replaceAnimations(pathData, *animations) 109 | } 110 | 111 | fun pathData(animations: List>): Builder { 112 | return replaceAnimations(pathData, animations) 113 | } 114 | 115 | // Fill type. 116 | 117 | fun fillType(fillType: FillType): Builder { 118 | this.fillType = fillType 119 | return self 120 | } 121 | 122 | // Clip type. 123 | 124 | fun clipType(clipType: ClipType): Builder { 125 | this.clipType = clipType 126 | return self 127 | } 128 | 129 | override val self = this 130 | 131 | override fun build(): ClipPathNode { 132 | return ClipPathNode( 133 | rotation, 134 | pivotX, 135 | pivotY, 136 | scaleX, 137 | scaleY, 138 | translateX, 139 | translateY, 140 | pathData, 141 | fillType, 142 | clipType 143 | ) 144 | } 145 | } 146 | 147 | // 148 | 149 | companion object { 150 | 151 | @JvmStatic 152 | fun builder(): Builder { 153 | return Builder() 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/ClipType.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | /** Determines the clipping strategy of a [ClipPathNode]. */ 4 | enum class ClipType { 5 | /** Only the pixels drawn inside the bounds of the clip path will be displayed. */ 6 | INTERSECT, 7 | /** Only the pixels drawn outside the bounds of the clip path will be displayed. */ 8 | SUBTRACT 9 | } 10 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/ComplexColor.java: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.res.ColorStateList; 6 | import android.content.res.Resources; 7 | import android.graphics.Shader; 8 | import android.util.AttributeSet; 9 | import android.util.Log; 10 | import android.util.Xml; 11 | 12 | import androidx.annotation.ColorInt; 13 | import androidx.annotation.ColorRes; 14 | import androidx.annotation.NonNull; 15 | import androidx.annotation.Nullable; 16 | import androidx.appcompat.content.res.AppCompatResources; 17 | 18 | import org.xmlpull.v1.XmlPullParser; 19 | import org.xmlpull.v1.XmlPullParserException; 20 | 21 | import java.io.IOException; 22 | 23 | import static android.graphics.Color.TRANSPARENT; 24 | 25 | /** 26 | * Represents a color which is one of either: 27 | * 28 | *
    29 | *
  1. A Gradient; as represented by a {@link Shader}. 30 | *
  2. A {@link ColorStateList} 31 | *
  3. A simple color represented by an {@code int} 32 | *
33 | */ 34 | final class ComplexColor { 35 | private static final String LOG_TAG = "ComplexColor"; 36 | 37 | @Nullable private final Shader mShader; 38 | @Nullable private final ColorStateList mColorStateList; 39 | private int mColor; // mutable for animation/state changes 40 | 41 | private ComplexColor( 42 | @Nullable Shader shader, @Nullable ColorStateList colorStateList, @ColorInt int color) { 43 | mShader = shader; 44 | mColorStateList = colorStateList; 45 | mColor = color; 46 | } 47 | 48 | static ComplexColor from(@NonNull Shader shader) { 49 | return new ComplexColor(shader, null, TRANSPARENT); 50 | } 51 | 52 | static ComplexColor from(@NonNull ColorStateList colorStateList) { 53 | return new ComplexColor(null, colorStateList, colorStateList.getDefaultColor()); 54 | } 55 | 56 | static ComplexColor from(@ColorInt int color) { 57 | return new ComplexColor(null, null, color); 58 | } 59 | 60 | @Nullable 61 | public Shader getShader() { 62 | return mShader; 63 | } 64 | 65 | @Nullable 66 | public ColorStateList getColorStateList() { 67 | return mColorStateList; 68 | } 69 | 70 | @ColorInt 71 | public int getColor() { 72 | return mColor; 73 | } 74 | 75 | public void setColor(@ColorInt int color) { 76 | mColor = color; 77 | } 78 | 79 | public boolean isGradient() { 80 | return mShader != null; 81 | } 82 | 83 | public boolean isStateful() { 84 | return mShader == null && mColorStateList != null && mColorStateList.isStateful(); 85 | } 86 | 87 | /** 88 | * @return {@code true} if the given state causes this color to change, otherwise {@code false}. 89 | * If the color has changed, it can be retrieved via {@link #getColor}. 90 | * @see #isStateful() 91 | * @see #getColor() 92 | */ 93 | public boolean onStateChanged(int[] stateSet) { 94 | boolean changed = false; 95 | if (isStateful()) { 96 | final int colorForState = 97 | mColorStateList.getColorForState(stateSet, mColorStateList.getDefaultColor()); 98 | if (colorForState != mColor) { 99 | changed = true; 100 | mColor = colorForState; 101 | } 102 | } 103 | return changed; 104 | } 105 | 106 | /** @return {@code true} if the this color will draw. */ 107 | public boolean willDraw() { 108 | return isGradient() || mColor != TRANSPARENT; 109 | } 110 | 111 | /** 112 | * Creates a ComplexColor from an XML document using given a set of {@link Resources} and a {@link 113 | * Resources.Theme}. 114 | * 115 | * @param context Context against which the ComplexColor should be inflated. 116 | * @param resId the resource identifier of the ColorStateList of GradientColor to retrieve. 117 | * @return A new color. 118 | */ 119 | @Nullable 120 | public static ComplexColor inflate(@NonNull Context context, @ColorRes int resId) { 121 | try { 122 | return createFromXml(context, resId); 123 | } catch (Exception e) { 124 | Log.e(LOG_TAG, "Failed to inflate ComplexColor.", e); 125 | } 126 | return null; 127 | } 128 | 129 | @NonNull 130 | private static ComplexColor createFromXml(@NonNull Context context, @ColorRes int resId) 131 | throws IOException, XmlPullParserException { 132 | @SuppressLint("ResourceType") 133 | XmlPullParser parser = context.getResources().getXml(resId); 134 | final AttributeSet attrs = Xml.asAttributeSet(parser); 135 | int type; 136 | while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { 137 | // Empty loop 138 | } 139 | if (type != XmlPullParser.START_TAG) { 140 | throw new XmlPullParserException("No start tag found"); 141 | } 142 | final String name = parser.getName(); 143 | switch (name) { 144 | case "selector": 145 | return ComplexColor.from(AppCompatResources.getColorStateList(context, resId)); 146 | case "gradient": 147 | return ComplexColor.from( 148 | GradientColorInflater.createFromXmlInner( 149 | context.getResources(), parser, attrs, context.getTheme())); 150 | default: 151 | throw new XmlPullParserException( 152 | parser.getPositionDescription() + ": unsupported complex color tag " + name); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/EllipseNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Path 4 | import android.graphics.RectF 5 | import androidx.annotation.FloatRange 6 | 7 | /** A [Node] that paints an ellipse. */ 8 | class EllipseNode private constructor( 9 | rotation: List>, 10 | pivotX: List>, 11 | pivotY: List>, 12 | scaleX: List>, 13 | scaleY: List>, 14 | translateX: List>, 15 | translateY: List>, 16 | fillColor: List>, 17 | fillColorComplex: ComplexColor?, 18 | fillAlpha: List>, 19 | strokeColor: List>, 20 | strokeColorComplex: ComplexColor?, 21 | strokeAlpha: List>, 22 | strokeWidth: List>, 23 | trimPathStart: List>, 24 | trimPathEnd: List>, 25 | trimPathOffset: List>, 26 | strokeLineCap: StrokeLineCap, 27 | strokeLineJoin: StrokeLineJoin, 28 | strokeMiterLimit: List>, 29 | strokeDashArray: List>, 30 | strokeDashOffset: List>, 31 | fillType: FillType, 32 | isStrokeScaling: Boolean, 33 | private val centerX: List>, 34 | private val centerY: List>, 35 | private val radiusX: List>, 36 | private val radiusY: List> 37 | ) : RenderNode( 38 | rotation, 39 | pivotX, 40 | pivotY, 41 | scaleX, 42 | scaleY, 43 | translateX, 44 | translateY, 45 | fillColor, 46 | fillColorComplex, 47 | fillAlpha, 48 | strokeColor, 49 | strokeColorComplex, 50 | strokeAlpha, 51 | strokeWidth, 52 | trimPathStart, 53 | trimPathEnd, 54 | trimPathOffset, 55 | strokeLineCap, 56 | strokeLineJoin, 57 | strokeMiterLimit, 58 | strokeDashArray, 59 | strokeDashOffset, 60 | fillType, 61 | isStrokeScaling 62 | ) { 63 | 64 | // 65 | 66 | override fun toLayer(timeline: PropertyTimeline): EllipseLayer { 67 | return EllipseLayer(timeline, this) 68 | } 69 | 70 | internal class EllipseLayer(timeline: PropertyTimeline, node: EllipseNode) : RenderNode.RenderLayer(timeline, node) { 71 | private val centerX = registerAnimatableProperty(node.centerX) 72 | private val centerY = registerAnimatableProperty(node.centerY) 73 | private val radiusX = registerAnimatableProperty(node.radiusX) 74 | private val radiusY = registerAnimatableProperty(node.radiusY) 75 | 76 | private val tempRect = RectF() 77 | 78 | override fun onInitPath(outPath: Path) { 79 | val cx = centerX.animatedValue 80 | val cy = centerY.animatedValue 81 | val rx = radiusX.animatedValue 82 | val ry = radiusY.animatedValue 83 | tempRect.set(cx - rx, cy - ry, cx + rx, cy + ry) 84 | outPath.addOval(tempRect, Path.Direction.CW) 85 | } 86 | } 87 | 88 | // 89 | 90 | // 91 | 92 | @DslMarker 93 | private annotation class EllipseNodeMarker 94 | 95 | /** Builder class used to create [EllipseNode]s. */ 96 | @EllipseNodeMarker 97 | class Builder internal constructor() : RenderNode.Builder() { 98 | private val centerX = asAnimations(0f) 99 | private val centerY = asAnimations(0f) 100 | private val radiusX = asAnimations(0f) 101 | private val radiusY = asAnimations(0f) 102 | 103 | // Center X. 104 | 105 | fun centerX(initialCenterX: Float): Builder { 106 | return replaceFirstAnimation(centerX, asAnimation(initialCenterX)) 107 | } 108 | 109 | @SafeVarargs 110 | fun centerX(vararg animations: Animation<*, Float>): Builder { 111 | return replaceAnimations(centerX, *animations) 112 | } 113 | 114 | fun centerX(animations: List>): Builder { 115 | return replaceAnimations(centerX, animations) 116 | } 117 | 118 | // Center Y. 119 | 120 | fun centerY(initialCenterY: Float): Builder { 121 | return replaceFirstAnimation(centerY, asAnimation(initialCenterY)) 122 | } 123 | 124 | @SafeVarargs 125 | fun centerY(vararg animations: Animation<*, Float>): Builder { 126 | return replaceAnimations(centerY, *animations) 127 | } 128 | 129 | fun centerY(animations: List>): Builder { 130 | return replaceAnimations(centerY, animations) 131 | } 132 | 133 | // Radius X. 134 | 135 | fun radiusX(@FloatRange(from = 0.0) initialRadiusX: Float): Builder { 136 | return replaceFirstAnimation(radiusX, asAnimation(initialRadiusX)) 137 | } 138 | 139 | @SafeVarargs 140 | fun radiusX(vararg animations: Animation<*, Float>): Builder { 141 | return replaceAnimations(radiusX, *animations) 142 | } 143 | 144 | fun radiusX(animations: List>): Builder { 145 | return replaceAnimations(radiusX, animations) 146 | } 147 | 148 | // Radius Y. 149 | 150 | fun radiusY(@FloatRange(from = 0.0) initialRadiusY: Float): Builder { 151 | return replaceFirstAnimation(radiusY, asAnimation(initialRadiusY)) 152 | } 153 | 154 | @SafeVarargs 155 | fun radiusY(vararg animations: Animation<*, Float>): Builder { 156 | return replaceAnimations(radiusY, *animations) 157 | } 158 | 159 | fun radiusY(animations: List>): Builder { 160 | return replaceAnimations(radiusY, animations) 161 | } 162 | 163 | override val self = this 164 | 165 | override fun build(): EllipseNode { 166 | return EllipseNode( 167 | rotation, 168 | pivotX, 169 | pivotY, 170 | scaleX, 171 | scaleY, 172 | translateX, 173 | translateY, 174 | fillColor, 175 | fillColorComplex, 176 | fillAlpha, 177 | strokeColor, 178 | strokeColorComplex, 179 | strokeAlpha, 180 | strokeWidth, 181 | trimPathStart, 182 | trimPathEnd, 183 | trimPathOffset, 184 | strokeLineCap, 185 | strokeLineJoin, 186 | strokeMiterLimit, 187 | strokeDashArray, 188 | strokeDashOffset, 189 | fillType, 190 | isScalingStroke, 191 | centerX, 192 | centerY, 193 | radiusX, 194 | radiusY 195 | ) 196 | } 197 | } 198 | 199 | // 200 | 201 | companion object { 202 | 203 | @JvmStatic 204 | fun builder(): Builder { 205 | return Builder() 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.view.animation.Interpolator 4 | import androidx.core.view.animation.PathInterpolatorCompat 5 | 6 | @JvmSynthetic 7 | inline fun kyrieDrawable(init: KyrieDrawable.Builder.() -> Unit): KyrieDrawable = 8 | KyrieDrawable.builder().apply(init).build() 9 | 10 | // KyrieDrawable.Builder children functions. 11 | 12 | @JvmSynthetic 13 | inline fun KyrieDrawable.Builder.circle(init: CircleNode.Builder.() -> Unit) { 14 | child(CircleNode.builder().apply(init)) 15 | } 16 | 17 | @JvmSynthetic 18 | inline fun KyrieDrawable.Builder.clipPath(init: ClipPathNode.Builder.() -> Unit) { 19 | child(ClipPathNode.builder().apply(init)) 20 | } 21 | 22 | @JvmSynthetic 23 | inline fun KyrieDrawable.Builder.ellipse(init: EllipseNode.Builder.() -> Unit) { 24 | child(EllipseNode.builder().apply(init)) 25 | } 26 | 27 | @JvmSynthetic 28 | inline fun KyrieDrawable.Builder.group(init: GroupNode.Builder.() -> Unit) { 29 | child(GroupNode.builder().apply(init)) 30 | } 31 | 32 | @JvmSynthetic 33 | inline fun KyrieDrawable.Builder.line(init: LineNode.Builder.() -> Unit) { 34 | child(LineNode.builder().apply(init)) 35 | } 36 | 37 | @JvmSynthetic 38 | inline fun KyrieDrawable.Builder.path(init: PathNode.Builder.() -> Unit) { 39 | child(PathNode.builder().apply(init)) 40 | } 41 | 42 | @JvmSynthetic 43 | inline fun KyrieDrawable.Builder.rectangle(init: RectangleNode.Builder.() -> Unit) { 44 | child(RectangleNode.builder().apply(init)) 45 | } 46 | 47 | // GroupNode.Builder children functions. 48 | 49 | @JvmSynthetic 50 | inline fun GroupNode.Builder.circle(init: CircleNode.Builder.() -> Unit): GroupNode.Builder = 51 | child(CircleNode.builder().apply(init)) 52 | 53 | @JvmSynthetic 54 | inline fun GroupNode.Builder.clipPath(init: ClipPathNode.Builder.() -> Unit): GroupNode.Builder = 55 | child(ClipPathNode.builder().apply(init)) 56 | 57 | @JvmSynthetic 58 | inline fun GroupNode.Builder.ellipse(init: EllipseNode.Builder.() -> Unit): GroupNode.Builder = 59 | child(EllipseNode.builder().apply(init)) 60 | 61 | @JvmSynthetic 62 | inline fun GroupNode.Builder.group(init: GroupNode.Builder.() -> Unit): GroupNode.Builder = 63 | child(GroupNode.builder().apply(init)) 64 | 65 | @JvmSynthetic 66 | inline fun GroupNode.Builder.line(init: LineNode.Builder.() -> Unit): GroupNode.Builder = 67 | child(LineNode.builder().apply(init)) 68 | 69 | @JvmSynthetic 70 | inline fun GroupNode.Builder.path(init: PathNode.Builder.() -> Unit): GroupNode.Builder = 71 | child(PathNode.builder().apply(init)) 72 | 73 | @JvmSynthetic 74 | inline fun GroupNode.Builder.rectangle(init: RectangleNode.Builder.() -> Unit): GroupNode.Builder = 75 | child(RectangleNode.builder().apply(init)) 76 | 77 | // Useful SVG path data extension functions. 78 | 79 | @JvmSynthetic 80 | fun String.asPath() = PathData.toPath(this) 81 | 82 | @JvmSynthetic 83 | fun String.asPathData() = PathData.parse(this) 84 | 85 | @JvmSynthetic 86 | fun String.asPathInterpolator(): Interpolator = PathInterpolatorCompat.create(PathData.toPath(this)) -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/FillType.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | /** Fill type determines how a shape should be filled when painted. */ 4 | enum class FillType { 5 | /** A non-zero winding rule. */ 6 | NON_ZERO, 7 | /** An even-odd winding rule. */ 8 | EVEN_ODD 9 | } 10 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/GradientColorInflater.java: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie; 2 | 3 | import android.content.res.Resources; 4 | import android.content.res.TypedArray; 5 | import android.graphics.LinearGradient; 6 | import android.graphics.RadialGradient; 7 | import android.graphics.Shader; 8 | import android.graphics.SweepGradient; 9 | import android.util.AttributeSet; 10 | 11 | import androidx.annotation.ColorInt; 12 | import androidx.annotation.IntDef; 13 | import androidx.annotation.NonNull; 14 | import androidx.annotation.Nullable; 15 | 16 | import org.xmlpull.v1.XmlPullParser; 17 | import org.xmlpull.v1.XmlPullParserException; 18 | 19 | import java.io.IOException; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | import static android.graphics.Color.TRANSPARENT; 26 | import static android.graphics.drawable.GradientDrawable.LINEAR_GRADIENT; 27 | import static android.graphics.drawable.GradientDrawable.RADIAL_GRADIENT; 28 | import static android.graphics.drawable.GradientDrawable.SWEEP_GRADIENT; 29 | 30 | final class GradientColorInflater { 31 | 32 | @IntDef({TILE_MODE_CLAMP, TILE_MODE_REPEAT, TILE_MODE_MIRROR}) 33 | @Retention(RetentionPolicy.SOURCE) 34 | private @interface GradientTileMode {} 35 | 36 | private static final int TILE_MODE_CLAMP = 0; 37 | private static final int TILE_MODE_REPEAT = 1; 38 | private static final int TILE_MODE_MIRROR = 2; 39 | 40 | private GradientColorInflater() {} 41 | 42 | static Shader createFromXmlInner( 43 | @NonNull Resources resources, 44 | @NonNull XmlPullParser parser, 45 | @NonNull AttributeSet attrs, 46 | @Nullable Resources.Theme theme) 47 | throws IOException, XmlPullParserException { 48 | final String name = parser.getName(); 49 | if (!name.equals("gradient")) { 50 | throw new XmlPullParserException( 51 | parser.getPositionDescription() + ": invalid gradient color tag " + name); 52 | } 53 | 54 | final TypedArray a = 55 | TypedArrayUtils.obtainAttributes(resources, theme, attrs, R.styleable.GradientColor); 56 | final float startX = 57 | TypedArrayUtils.getNamedFloat( 58 | a, parser, "startX", R.styleable.GradientColor_android_startX, 0f); 59 | final float startY = 60 | TypedArrayUtils.getNamedFloat( 61 | a, parser, "startY", R.styleable.GradientColor_android_startY, 0f); 62 | final float endX = 63 | TypedArrayUtils.getNamedFloat( 64 | a, parser, "endX", R.styleable.GradientColor_android_endX, 0f); 65 | final float endY = 66 | TypedArrayUtils.getNamedFloat( 67 | a, parser, "endY", R.styleable.GradientColor_android_endY, 0f); 68 | final float centerX = 69 | TypedArrayUtils.getNamedFloat( 70 | a, parser, "centerX", R.styleable.GradientColor_android_centerX, 0f); 71 | final float centerY = 72 | TypedArrayUtils.getNamedFloat( 73 | a, parser, "centerY", R.styleable.GradientColor_android_centerY, 0f); 74 | final int type = 75 | TypedArrayUtils.getNamedInt( 76 | a, parser, "type", R.styleable.GradientColor_android_type, LINEAR_GRADIENT); 77 | final int startColor = 78 | TypedArrayUtils.getNamedColor( 79 | a, parser, "startColor", R.styleable.GradientColor_android_startColor, TRANSPARENT); 80 | final boolean hasCenterColor = TypedArrayUtils.hasAttribute(parser, "centerColor"); 81 | final int centerColor = 82 | TypedArrayUtils.getNamedColor( 83 | a, parser, "centerColor", R.styleable.GradientColor_android_centerColor, TRANSPARENT); 84 | final int endColor = 85 | TypedArrayUtils.getNamedColor( 86 | a, parser, "endColor", R.styleable.GradientColor_android_endColor, TRANSPARENT); 87 | final int tileMode = 88 | TypedArrayUtils.getNamedInt( 89 | a, parser, "tileMode", R.styleable.GradientColor_android_tileMode, TILE_MODE_CLAMP); 90 | final float gradientRadius = 91 | TypedArrayUtils.getNamedFloat( 92 | a, parser, "gradientRadius", R.styleable.GradientColor_android_gradientRadius, 0f); 93 | a.recycle(); 94 | 95 | ColorStops colorStops = inflateChildElements(resources, parser, attrs, theme); 96 | colorStops = checkColors(colorStops, startColor, endColor, hasCenterColor, centerColor); 97 | 98 | switch (type) { 99 | case RADIAL_GRADIENT: 100 | if (gradientRadius <= 0f) { 101 | throw new XmlPullParserException( 102 | " tag requires 'gradientRadius' attribute with radial type"); 103 | } 104 | return new RadialGradient( 105 | centerX, 106 | centerY, 107 | gradientRadius, 108 | colorStops.mColors, 109 | colorStops.mOffsets, 110 | parseTileMode(tileMode)); 111 | case SWEEP_GRADIENT: 112 | return new SweepGradient(centerX, centerY, colorStops.mColors, colorStops.mOffsets); 113 | case LINEAR_GRADIENT: 114 | default: 115 | return new LinearGradient( 116 | startX, 117 | startY, 118 | endX, 119 | endY, 120 | colorStops.mColors, 121 | colorStops.mOffsets, 122 | parseTileMode(tileMode)); 123 | } 124 | } 125 | 126 | private static ColorStops inflateChildElements( 127 | @NonNull Resources resources, 128 | @NonNull XmlPullParser parser, 129 | @NonNull AttributeSet attrs, 130 | @Nullable Resources.Theme theme) 131 | throws XmlPullParserException, IOException { 132 | final int innerDepth = parser.getDepth() + 1; 133 | int type; 134 | int depth; 135 | 136 | List offsets = new ArrayList<>(20); 137 | List colors = new ArrayList<>(20); 138 | 139 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 140 | && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 141 | if (type != XmlPullParser.START_TAG) { 142 | continue; 143 | } 144 | if (depth > innerDepth || !parser.getName().equals("item")) { 145 | continue; 146 | } 147 | 148 | final TypedArray a = 149 | TypedArrayUtils.obtainAttributes(resources, theme, attrs, R.styleable.GradientColorItem); 150 | final boolean hasColor = a.hasValue(R.styleable.GradientColorItem_android_color); 151 | final boolean hasOffset = a.hasValue(R.styleable.GradientColorItem_android_offset); 152 | if (!hasColor || !hasOffset) { 153 | throw new XmlPullParserException( 154 | parser.getPositionDescription() 155 | + ": tag requires a 'color' attribute and a 'offset' " 156 | + "attribute!"); 157 | } 158 | 159 | final int color = a.getColor(R.styleable.GradientColorItem_android_color, TRANSPARENT); 160 | final float offset = a.getFloat(R.styleable.GradientColorItem_android_offset, 0f); 161 | a.recycle(); 162 | 163 | colors.add(color); 164 | offsets.add(offset); 165 | } 166 | if (colors.size() > 0) return new ColorStops(colors, offsets); 167 | return null; 168 | } 169 | 170 | private static ColorStops checkColors( 171 | @Nullable ColorStops colorItems, 172 | @ColorInt int startColor, 173 | @ColorInt int endColor, 174 | boolean hasCenterColor, 175 | @ColorInt int centerColor) { 176 | // prefer child color items if any, otherwise use the start, (center), end colors 177 | if (colorItems != null) { 178 | return colorItems; 179 | } else if (hasCenterColor) { 180 | return new ColorStops(startColor, centerColor, endColor); 181 | } else { 182 | return new ColorStops(startColor, endColor); 183 | } 184 | } 185 | 186 | private static Shader.TileMode parseTileMode(@GradientTileMode int tileMode) { 187 | switch (tileMode) { 188 | case TILE_MODE_REPEAT: 189 | return Shader.TileMode.REPEAT; 190 | case TILE_MODE_MIRROR: 191 | return Shader.TileMode.MIRROR; 192 | case TILE_MODE_CLAMP: 193 | default: 194 | return Shader.TileMode.CLAMP; 195 | } 196 | } 197 | 198 | static final class ColorStops { 199 | final int[] mColors; 200 | final float[] mOffsets; 201 | 202 | ColorStops(@NonNull List colorsList, @NonNull List offsetsList) { 203 | final int size = colorsList.size(); 204 | mColors = new int[size]; 205 | mOffsets = new float[size]; 206 | for (int i = 0; i < size; i++) { 207 | mColors[i] = colorsList.get(i); 208 | mOffsets[i] = offsetsList.get(i); 209 | } 210 | } 211 | 212 | ColorStops(@ColorInt int startColor, @ColorInt int endColor) { 213 | mColors = new int[] {startColor, endColor}; 214 | mOffsets = new float[] {0f, 1f}; 215 | } 216 | 217 | ColorStops(@ColorInt int startColor, @ColorInt int centerColor, @ColorInt int endColor) { 218 | mColors = new int[] {startColor, centerColor, endColor}; 219 | mOffsets = new float[] {0f, 0.5f, 1f}; 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/GroupNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Matrix 5 | import android.graphics.PointF 6 | 7 | /** A [Node] that holds a group of children [Node]s. */ 8 | class GroupNode private constructor( 9 | rotation: List>, 10 | pivotX: List>, 11 | pivotY: List>, 12 | scaleX: List>, 13 | scaleY: List>, 14 | translateX: List>, 15 | translateY: List>, 16 | private val children: List 17 | ) : TransformNode(rotation, pivotX, pivotY, scaleX, scaleY, translateX, translateY) { 18 | 19 | // 20 | 21 | override fun toLayer(timeline: PropertyTimeline): GroupLayer { 22 | return GroupLayer(timeline, this) 23 | } 24 | 25 | internal class GroupLayer(timeline: PropertyTimeline, node: GroupNode) : TransformNode.TransformLayer(timeline, node) { 26 | private val children: ArrayList 27 | 28 | init { 29 | val childrenNodes = node.children 30 | children = ArrayList(childrenNodes.size) 31 | var i = 0 32 | val size = childrenNodes.size 33 | while (i < size) { 34 | children.add(childrenNodes[i].toLayer(timeline)) 35 | i++ 36 | } 37 | } 38 | 39 | override fun onDraw(canvas: Canvas, parentMatrix: Matrix, viewportScale: PointF) { 40 | canvas.save() 41 | children.forEach { it.draw(canvas, parentMatrix, viewportScale) } 42 | canvas.restore() 43 | } 44 | 45 | override fun isStateful(): Boolean { 46 | for (i in 0 until children.size) { 47 | if (children[i].isStateful()) { 48 | return true 49 | } 50 | } 51 | return false 52 | } 53 | 54 | override fun onStateChange(stateSet: IntArray): Boolean { 55 | var changed = false 56 | for (i in 0 until children.size) { 57 | changed = changed or children[i].onStateChange(stateSet) 58 | } 59 | return changed 60 | } 61 | } 62 | 63 | // 64 | 65 | // 66 | 67 | @DslMarker 68 | private annotation class GroupNodeMarker 69 | 70 | /** Builder class used to create [GroupNode]s. */ 71 | @GroupNodeMarker 72 | class Builder internal constructor() : TransformNode.Builder() { 73 | private val children = ArrayList() 74 | 75 | // Children. 76 | 77 | fun child(node: Node): Builder { 78 | children.add(node) 79 | return this 80 | } 81 | 82 | fun child(builder: Node.Builder<*>): Builder { 83 | return child(builder.build()) 84 | } 85 | 86 | override val self = this 87 | 88 | override fun build(): GroupNode { 89 | return GroupNode(rotation, pivotX, pivotY, scaleX, scaleY, translateX, translateY, children) 90 | } 91 | } 92 | 93 | // 94 | 95 | companion object { 96 | 97 | @JvmStatic 98 | fun builder(): Builder { 99 | return Builder() 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/Keyframe.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.animation.TimeInterpolator 4 | import androidx.annotation.FloatRange 5 | 6 | /** 7 | * This class holds a time/value pair for an animation. A [Keyframe] is used to define the 8 | * values that the animation target will have over the course of the animation. As the time proceeds 9 | * from one keyframe to the other, the value of the target will animate between the value at the 10 | * previous keyframe and the value at the next keyframe. Each keyframe also holds an optional 11 | * [TimeInterpolator] object, which defines the time interpolation over the inter-value preceding 12 | * the keyframe. 13 | * 14 | * @param T The keyframe value type. 15 | */ 16 | class Keyframe private constructor(@FloatRange(from = 0.0, to = 1.0) fraction: Float, value: T?) { 17 | 18 | /** 19 | * Gets the time for this [Keyframe], as a fraction of the overall animation duration. 20 | * 21 | * @return The time associated with this [Keyframe], as a fraction of the overall animation 22 | * duration. This should be a value between 0 and 1. 23 | */ 24 | @FloatRange(from = 0.0, to = 1.0) 25 | @get:FloatRange(from = 0.0, to = 1.0) 26 | var fraction: Float = 0f 27 | private set 28 | 29 | /** 30 | * Gets the value for this [Keyframe]. 31 | * 32 | * @return The value for this [Keyframe]. 33 | */ 34 | var value: T? = null 35 | private set 36 | 37 | /** 38 | * Gets the optional interpolator for this [Keyframe]. A value of null indicates that there 39 | * is no interpolation, which is the same as linear interpolation. 40 | * 41 | * @return The optional interpolator for this [Keyframe]. May be null. 42 | */ 43 | var interpolator: TimeInterpolator? = null 44 | private set 45 | 46 | init { 47 | this.fraction = fraction 48 | this.value = value 49 | } 50 | 51 | /** 52 | * Sets the time for this [Keyframe], as a fraction of the overall animation duration. 53 | * 54 | * @param fraction The time associated with this [Keyframe], as a fraction of the overall 55 | * animation duration. This should be a value between 0 and 1. 56 | * @return This [Keyframe] object (to allow for chaining of calls to setter methods). 57 | */ 58 | fun fraction(@FloatRange(from = 0.0, to = 1.0) fraction: Float): Keyframe { 59 | this.fraction = fraction 60 | return this 61 | } 62 | 63 | /** 64 | * Sets the value for this [Keyframe]. 65 | * 66 | * @param value The value for this [Keyframe]. May be null. 67 | * @return This [Keyframe] object (to allow for chaining of calls to setter methods). 68 | */ 69 | fun value(value: T?): Keyframe { 70 | this.value = value 71 | return this 72 | } 73 | 74 | /** 75 | * Sets the optional interpolator for this [Keyframe]. A value of null indicates that there 76 | * is no interpolation, which is the same as linear interpolation. 77 | * 78 | * @param interpolator The optional interpolator for this [Keyframe]. May be null. 79 | * @return This [Keyframe] object (to allow for chaining of calls to setter methods). 80 | */ 81 | fun interpolator(interpolator: TimeInterpolator?): Keyframe { 82 | this.interpolator = interpolator 83 | return this 84 | } 85 | 86 | companion object { 87 | 88 | /** 89 | * Constructs a [Keyframe] object with the given time. The value at this time will be 90 | * derived from the target object when the animation first starts. The time defines the time, as a 91 | * proportion of an overall animation's duration, at which the value will hold true for the 92 | * animation. The value for the animation between keyframes will be calculated as an interpolation 93 | * between the values at those keyframes. 94 | * 95 | * @param T The keyframe value type. 96 | * @param fraction The time, expressed as a value between 0 and 1, representing the fraction of 97 | * time elapsed of the overall animation duration. 98 | * @return The constructed [Keyframe] object. 99 | */ 100 | @JvmStatic 101 | fun of(@FloatRange(from = 0.0, to = 1.0) fraction: Float): Keyframe { 102 | return of(fraction, null) 103 | } 104 | 105 | /** 106 | * Constructs a [Keyframe] object with the given time and value. The time defines the time, 107 | * as a proportion of an overall animation's duration, at which the value will hold true for the 108 | * animation. The value for the animation between keyframes will be calculated as an interpolation 109 | * between the values at those keyframes. 110 | * 111 | * @param T The keyframe value type. 112 | * @param fraction The time, expressed as a value between 0 and 1, representing the fraction of 113 | * time elapsed of the overall animation duration. 114 | * @param value The value that the object will animate to as the animation time approaches the 115 | * time in this [Keyframe], and the the value animated from as the time passes the time 116 | * in this [Keyframe]. May be null. 117 | * @return The constructed [Keyframe] object. 118 | */ 119 | @JvmStatic 120 | fun of(@FloatRange(from = 0.0, to = 1.0) fraction: Float, value: T?): Keyframe { 121 | return Keyframe(fraction, value) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/KeyframeSet.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Path 4 | import android.graphics.PointF 5 | import com.github.alexjlockwood.kyrie.Animation.ValueEvaluator 6 | import java.util.Arrays 7 | 8 | /** 9 | * Abstracts a collection of [Keyframe] objects and is used to calculate values between those 10 | * keyframes for a given [Animation]. 11 | * 12 | * @param T The keyframe value type. 13 | */ 14 | internal abstract class KeyframeSet { 15 | 16 | /** @return The list of keyframes contained by this keyframe set. */ 17 | abstract val keyframes: List> 18 | 19 | /** 20 | * Gets the animated value, given the elapsed fraction of the animation (interpolated by the 21 | * animation's interpolator) and the evaluator used to calculate in-between values. This function 22 | * maps the input fraction to the appropriate keyframe interval and a fraction between them and 23 | * returns the interpolated value. Note that the input fraction may fall outside the [0,1] bounds, 24 | * if the animation's interpolator made that happen (e.g., a spring interpolation that might send 25 | * the fraction past 1.0). We handle this situation by just using the two keyframes at the 26 | * appropriate end when the value is outside those bounds. 27 | * 28 | * @param fraction The elapsed fraction of the animation. 29 | * @return The animated value. 30 | */ 31 | abstract fun getAnimatedValue(fraction: Float): T 32 | 33 | companion object { 34 | private val KEYFRAME_COMPARATOR = Comparator> { k1, k2 -> k1.fraction.compareTo(k2.fraction) } 35 | 36 | /** @return An [ObjectKeyframeSet] with evenly distributed keyframe values. */ 37 | fun ofObject(evaluator: ValueEvaluator, values: Array): KeyframeSet { 38 | val numKeyframes = values.size 39 | val keyframes = ArrayList>(Math.max(numKeyframes, 2)) 40 | if (numKeyframes == 1) { 41 | keyframes.add(Keyframe.of(0f)) 42 | keyframes.add(Keyframe.of(1f, values[0])) 43 | } else { 44 | keyframes.add(Keyframe.of(0f, values[0])) 45 | for (i in 1 until numKeyframes) { 46 | keyframes.add(Keyframe.of(i.toFloat() / (numKeyframes - 1), values[i])) 47 | } 48 | } 49 | return ObjectKeyframeSet(evaluator, keyframes) 50 | } 51 | 52 | /** @return An [ObjectKeyframeSet] with the given keyframe values. */ 53 | fun ofObject(evaluator: ValueEvaluator, values: Array>): KeyframeSet { 54 | Arrays.sort(values, KEYFRAME_COMPARATOR) 55 | val list = ArrayList>(values.size) 56 | val seenFractions = HashSet(values.size) 57 | for (i in values.indices.reversed()) { 58 | if (!seenFractions.contains(values[i].fraction)) { 59 | list.add(values[i]) 60 | seenFractions.add(values[i].fraction) 61 | } 62 | } 63 | list.reverse() 64 | return ObjectKeyframeSet(evaluator, list) 65 | } 66 | 67 | /** @return A [PathKeyframeSet] that estimates motion along the given path. */ 68 | fun ofPath(path: Path): KeyframeSet { 69 | return PathKeyframeSet(path) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/LineNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Path 4 | 5 | /** A [Node] that paints a line. */ 6 | class LineNode private constructor( 7 | rotation: List>, 8 | pivotX: List>, 9 | pivotY: List>, 10 | scaleX: List>, 11 | scaleY: List>, 12 | translateX: List>, 13 | translateY: List>, 14 | fillColor: List>, 15 | fillColorComplex: ComplexColor?, 16 | fillAlpha: List>, 17 | strokeColor: List>, 18 | strokeColorComplex: ComplexColor?, 19 | strokeAlpha: List>, 20 | strokeWidth: List>, 21 | trimPathStart: List>, 22 | trimPathEnd: List>, 23 | trimPathOffset: List>, 24 | strokeLineCap: StrokeLineCap, 25 | strokeLineJoin: StrokeLineJoin, 26 | strokeMiterLimit: List>, 27 | strokeDashArray: List>, 28 | strokeDashOffset: List>, 29 | fillType: FillType, 30 | isStrokeScaling: Boolean, 31 | private val startX: List>, 32 | private val startY: List>, 33 | private val endX: List>, 34 | private val endY: List> 35 | ) : RenderNode( 36 | rotation, 37 | pivotX, 38 | pivotY, 39 | scaleX, 40 | scaleY, 41 | translateX, 42 | translateY, 43 | fillColor, 44 | fillColorComplex, 45 | fillAlpha, 46 | strokeColor, 47 | strokeColorComplex, 48 | strokeAlpha, 49 | strokeWidth, 50 | trimPathStart, 51 | trimPathEnd, 52 | trimPathOffset, 53 | strokeLineCap, 54 | strokeLineJoin, 55 | strokeMiterLimit, 56 | strokeDashArray, 57 | strokeDashOffset, 58 | fillType, 59 | isStrokeScaling 60 | ) { 61 | 62 | // 63 | 64 | override fun toLayer(timeline: PropertyTimeline): LineLayer { 65 | return LineLayer(timeline, this) 66 | } 67 | 68 | internal class LineLayer(timeline: PropertyTimeline, node: LineNode) : RenderNode.RenderLayer(timeline, node) { 69 | private val startX = registerAnimatableProperty(node.startX) 70 | private val startY = registerAnimatableProperty(node.startY) 71 | private val endX = registerAnimatableProperty(node.endX) 72 | private val endY = registerAnimatableProperty(node.endY) 73 | 74 | override fun onInitPath(outPath: Path) { 75 | val startX = this.startX.animatedValue 76 | val startY = this.startY.animatedValue 77 | val endX = this.endX.animatedValue 78 | val endY = this.endY.animatedValue 79 | outPath.moveTo(startX, startY) 80 | outPath.lineTo(endX, endY) 81 | } 82 | } 83 | 84 | // 85 | 86 | // 87 | 88 | @DslMarker 89 | private annotation class LineNodeMarker 90 | 91 | /** Builder class used to create [LineNode]s. */ 92 | @LineNodeMarker 93 | class Builder internal constructor() : RenderNode.Builder() { 94 | private val startX = asAnimations(0f) 95 | private val startY = asAnimations(0f) 96 | private val endX = asAnimations(0f) 97 | private val endY = asAnimations(0f) 98 | 99 | // Start X. 100 | 101 | fun startX(initialStartX: Float): Builder { 102 | return replaceFirstAnimation(startX, asAnimation(initialStartX)) 103 | } 104 | 105 | @SafeVarargs 106 | fun startX(vararg animations: Animation<*, Float>): Builder { 107 | return replaceAnimations(startX, *animations) 108 | } 109 | 110 | fun startX(animations: List>): Builder { 111 | return replaceAnimations(startX, animations) 112 | } 113 | 114 | // Start Y. 115 | 116 | fun startY(initialStartY: Float): Builder { 117 | return replaceFirstAnimation(startY, asAnimation(initialStartY)) 118 | } 119 | 120 | @SafeVarargs 121 | fun startY(vararg animations: Animation<*, Float>): Builder { 122 | return replaceAnimations(startY, *animations) 123 | } 124 | 125 | fun startY(animations: List>): Builder { 126 | return replaceAnimations(startY, animations) 127 | } 128 | 129 | // End X. 130 | 131 | fun endX(initialEndX: Float): Builder { 132 | return replaceFirstAnimation(endX, asAnimation(initialEndX)) 133 | } 134 | 135 | @SafeVarargs 136 | fun endX(vararg animations: Animation<*, Float>): Builder { 137 | return replaceAnimations(endX, *animations) 138 | } 139 | 140 | fun endX(animations: List>): Builder { 141 | return replaceAnimations(endX, animations) 142 | } 143 | 144 | // End Y. 145 | 146 | fun endY(initialEndY: Float): Builder { 147 | return replaceFirstAnimation(endY, asAnimation(initialEndY)) 148 | } 149 | 150 | @SafeVarargs 151 | fun endY(vararg animations: Animation<*, Float>): Builder { 152 | return replaceAnimations(endY, *animations) 153 | } 154 | 155 | fun endY(animations: List>): Builder { 156 | return replaceAnimations(endY, animations) 157 | } 158 | 159 | override val self = this 160 | 161 | override fun build(): LineNode { 162 | return LineNode( 163 | rotation, 164 | pivotX, 165 | pivotY, 166 | scaleX, 167 | scaleY, 168 | translateX, 169 | translateY, 170 | fillColor, 171 | fillColorComplex, 172 | fillAlpha, 173 | strokeColor, 174 | strokeColorComplex, 175 | strokeAlpha, 176 | strokeWidth, 177 | trimPathStart, 178 | trimPathEnd, 179 | trimPathOffset, 180 | strokeLineCap, 181 | strokeLineJoin, 182 | strokeMiterLimit, 183 | strokeDashArray, 184 | strokeDashOffset, 185 | fillType, 186 | isScalingStroke, 187 | startX, 188 | startY, 189 | endX, 190 | endY 191 | ) 192 | } 193 | } 194 | 195 | // 196 | 197 | companion object { 198 | 199 | @JvmStatic 200 | fun builder(): Builder { 201 | return Builder() 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/Node.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Matrix 5 | import android.graphics.PointF 6 | import androidx.annotation.ColorInt 7 | import java.util.Collections 8 | 9 | /** Base class for all [Node]s used to construct and animate a [KyrieDrawable]. */ 10 | abstract class Node internal constructor() { 11 | 12 | /** 13 | * Constructs a [Layer] using the information contained by this [Node]. 14 | * 15 | * @param timeline The [PropertyTimeline] to use to register property animations. 16 | * @return A new [Layer] representing this [Node]. 17 | */ 18 | internal abstract fun toLayer(timeline: PropertyTimeline): Layer 19 | 20 | internal interface Layer { 21 | fun draw(canvas: Canvas, parentMatrix: Matrix, viewportScale: PointF) 22 | 23 | fun onDraw(canvas: Canvas, parentMatrix: Matrix, viewportScale: PointF) 24 | 25 | fun isStateful(): Boolean 26 | 27 | fun onStateChange(stateSet: IntArray): Boolean 28 | } 29 | 30 | @DslMarker 31 | private annotation class NodeMarker 32 | 33 | /** 34 | * Base class for all [Node.Builder]s used to construct new [Node] instances. 35 | * 36 | * @param B The concrete builder subclass type. 37 | */ 38 | @NodeMarker 39 | abstract class Builder> internal constructor() { 40 | internal abstract val self: B 41 | 42 | internal abstract fun build(): Node 43 | 44 | internal fun replaceFirstAnimation( 45 | animations: MutableList>, animation: Animation<*, T>): B { 46 | Node.replaceFirstAnimation(animations, animation) 47 | return self 48 | } 49 | 50 | @SafeVarargs 51 | internal fun replaceAnimations( 52 | animations: MutableList>, vararg newAnimations: Animation<*, T>): B { 53 | Node.replaceAnimations(animations, *newAnimations) 54 | return self 55 | } 56 | 57 | internal fun replaceAnimations( 58 | animations: MutableList>, newAnimations: List>): B { 59 | Node.replaceAnimations(animations, newAnimations) 60 | return self 61 | } 62 | } 63 | 64 | internal companion object { 65 | 66 | internal fun asAnimation(initialValue: Float): Animation<*, Float> { 67 | return Animation.ofFloat(initialValue, initialValue).duration(0) 68 | } 69 | 70 | internal fun asAnimation(@ColorInt initialValue: Int): Animation<*, Int> { 71 | return Animation.ofArgb(initialValue, initialValue).duration(0) 72 | } 73 | 74 | internal fun asAnimation(initialValue: FloatArray): Animation<*, FloatArray> { 75 | return Animation.ofFloatArray(initialValue, initialValue).duration(0) 76 | } 77 | 78 | internal fun asAnimation(initialValue: PathData): Animation<*, PathData> { 79 | return Animation.ofPathMorph(initialValue, initialValue).duration(0) 80 | } 81 | 82 | internal fun asAnimations(initialValue: Float): MutableList> { 83 | return mutableListOf(asAnimation(initialValue)) 84 | } 85 | 86 | internal fun asAnimations(initialValue: Int): MutableList> { 87 | return mutableListOf(asAnimation(initialValue)) 88 | } 89 | 90 | internal fun asAnimations(initialValue: FloatArray): MutableList> { 91 | return mutableListOf(asAnimation(initialValue)) 92 | } 93 | 94 | internal fun asAnimations(initialValue: PathData): MutableList> { 95 | return mutableListOf(asAnimation(initialValue)) 96 | } 97 | 98 | internal fun replaceFirstAnimation(animations: MutableList>, animation: Animation<*, T>) { 99 | animations[0] = animation 100 | } 101 | 102 | internal fun replaceAnimations(animations: MutableList>, vararg newAnimations: Animation<*, T>) { 103 | for (i in animations.size - 1 downTo 1) { 104 | animations.removeAt(i) 105 | } 106 | Collections.addAll(animations, *newAnimations) 107 | } 108 | 109 | internal fun replaceAnimations(animations: MutableList>, newAnimations: List>) { 110 | for (i in animations.size - 1 downTo 1) { 111 | animations.removeAt(i) 112 | } 113 | animations.addAll(newAnimations) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/ObjectKeyframeSet.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import com.github.alexjlockwood.kyrie.Animation.ValueEvaluator 4 | 5 | /** 6 | * Abstracts a collection of [Keyframe] objects and is used to calculate values between those 7 | * keyframes for a given [Animation]. 8 | * 9 | * @param T The keyframe value type. 10 | */ 11 | internal class ObjectKeyframeSet( 12 | private val evaluator: ValueEvaluator, 13 | // Only used when there are more than 2 keyframes. 14 | override val keyframes: List> 15 | ) : KeyframeSet() { 16 | 17 | private val firstKf = keyframes.first() 18 | private val lastKf = keyframes.last() 19 | // Only used in the 2-keyframe case. 20 | private val interpolator = lastKf.interpolator 21 | 22 | override fun getAnimatedValue(fraction: Float): T { 23 | val numKeyframes = keyframes.size 24 | var fraction = fraction 25 | // Special-case optimization for the common case of only two keyframes. 26 | if (numKeyframes == 2) { 27 | if (interpolator != null) { 28 | fraction = interpolator.getInterpolation(fraction) 29 | } 30 | return evaluator.evaluate(fraction, firstKf.value!!, lastKf.value!!) 31 | } 32 | if (fraction <= 0) { 33 | val nextKf = keyframes[1] 34 | val interpolator = nextKf.interpolator 35 | if (interpolator != null) { 36 | fraction = interpolator.getInterpolation(fraction) 37 | } 38 | val prevFraction = firstKf.fraction 39 | val intervalFraction = (fraction - prevFraction) / (nextKf.fraction - prevFraction) 40 | return evaluator.evaluate(intervalFraction, firstKf.value!!, nextKf.value!!) 41 | } 42 | if (fraction >= 1) { 43 | val prefKf = keyframes[numKeyframes - 2] 44 | val interpolator = lastKf.interpolator 45 | if (interpolator != null) { 46 | fraction = interpolator.getInterpolation(fraction) 47 | } 48 | val prevFraction = prefKf.fraction 49 | val intervalFraction = (fraction - prevFraction) / (lastKf.fraction - prevFraction) 50 | return evaluator.evaluate(intervalFraction, prefKf.value!!, lastKf.value!!) 51 | } 52 | var prevKf = firstKf 53 | for (i in 1 until numKeyframes) { 54 | val nextKf = keyframes[i] 55 | if (fraction < nextKf.fraction) { 56 | val interpolator = nextKf.interpolator 57 | val prevFraction = prevKf.fraction 58 | var intervalFraction = (fraction - prevFraction) / (nextKf.fraction - prevFraction) 59 | // Apply getInterpolator on the proportional duration. 60 | if (interpolator != null) { 61 | intervalFraction = interpolator.getInterpolation(intervalFraction) 62 | } 63 | return evaluator.evaluate(intervalFraction, prevKf.value!!, nextKf.value!!) 64 | } 65 | prevKf = nextKf 66 | } 67 | // Shouldn't get here. 68 | return lastKf.value!! 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/PathData.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Path 4 | 5 | import java.util.Arrays 6 | 7 | private val EMPTY_PATH_DATUMS = arrayOf() 8 | 9 | /** A simple container class that represents an SVG path string. */ 10 | class PathData { 11 | 12 | internal val pathDatums: Array 13 | 14 | @JvmOverloads 15 | internal constructor(pathDatums: Array = EMPTY_PATH_DATUMS) { 16 | this.pathDatums = pathDatums 17 | } 18 | 19 | internal constructor(pathData: PathData) { 20 | pathDatums = pathData.pathDatums.map { PathDatum(it) }.toTypedArray() 21 | } 22 | 23 | /** 24 | * Checks if this [PathData] object is morphable with another [PathData] object. 25 | * 26 | * @param pathData The [PathData] object to compare against. 27 | * @return true iff this [PathData] object is morphable with the provided [PathData] 28 | * object. 29 | */ 30 | fun canMorphWith(pathData: PathData): Boolean { 31 | return PathDataUtils.canMorph(this, pathData) 32 | } 33 | 34 | /** 35 | * Interpolates this [PathData] object between two [PathData] objects by the given 36 | * fraction. 37 | * 38 | * @param from The starting [PathData] object. 39 | * @param to The ending [PathData] object. 40 | * @param fraction The interpolation fraction. 41 | * @throws IllegalArgumentException If the from or to [PathData] arguments aren't morphable 42 | * with this [PathData] object. 43 | */ 44 | internal fun interpolate(from: PathData, to: PathData, fraction: Float) { 45 | if (!canMorphWith(from) || !canMorphWith(to)) { 46 | throw IllegalArgumentException("Can't interpolate between two incompatible paths") 47 | } 48 | for (i in from.pathDatums.indices) { 49 | pathDatums[i].interpolate(from.pathDatums[i], to.pathDatums[i], fraction) 50 | } 51 | } 52 | 53 | /** Each PathDatum object represents one command in the "d" attribute of an SVG pathData. */ 54 | internal class PathDatum { 55 | 56 | var type: Char = ' ' 57 | var params: FloatArray 58 | 59 | constructor(type: Char, params: FloatArray) { 60 | this.type = type 61 | this.params = params 62 | } 63 | 64 | constructor(n: PathDatum) { 65 | type = n.type 66 | params = Arrays.copyOfRange(n.params, 0, n.params.size) 67 | } 68 | 69 | /** 70 | * The current PathDatum will be interpolated between the from and to values according to the 71 | * current fraction. 72 | * 73 | * @param from The start value as a PathDatum. 74 | * @param to The end value as a PathDatum 75 | * @param fraction The fraction to interpolate. 76 | */ 77 | fun interpolate(from: PathDatum, to: PathDatum, fraction: Float) { 78 | for (i in from.params.indices) { 79 | params[i] = from.params[i] * (1 - fraction) + to.params[i] * fraction 80 | } 81 | } 82 | } 83 | 84 | companion object { 85 | 86 | /** 87 | * Constructs a [PathData] object from the provided SVG path data string. 88 | * 89 | * @param pathData The SVG path data string to convert. 90 | * @return A [PathData] object represented by the provided SVG path data string. 91 | */ 92 | @JvmStatic 93 | fun parse(pathData: String): PathData { 94 | return PathDataUtils.parse(pathData) 95 | } 96 | 97 | /** 98 | * Constructs a [Path] from the provided [PathData] object. 99 | * 100 | * @param pathData The SVG path data string to convert. 101 | * @return A [Path] represented by the provided SVG path data string. 102 | */ 103 | @JvmStatic 104 | fun toPath(pathData: String): Path { 105 | return PathDataUtils.toPath(pathData) 106 | } 107 | 108 | /** 109 | * Constructs a [Path] from the provided [PathData] object. 110 | * 111 | * @param pathData The [PathData] object to convert. 112 | * @return A [Path] represented by the provided [PathData] object. 113 | */ 114 | @JvmStatic 115 | fun toPath(pathData: PathData): Path { 116 | val path = Path() 117 | PathDataUtils.toPath(pathData, path) 118 | return path 119 | } 120 | 121 | /** 122 | * Initializes a [Path] from the provided [PathData] object. 123 | * 124 | * @param pathData The [PathData] object to convert. 125 | * @param outPath The [Path] to write to. 126 | */ 127 | @JvmStatic 128 | fun toPath(pathData: PathData, outPath: Path) { 129 | PathDataUtils.toPath(pathData, outPath) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/PathKeyframeSet.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Path 4 | import android.graphics.PathMeasure 5 | import android.graphics.PointF 6 | import android.os.Build 7 | import androidx.annotation.FloatRange 8 | import androidx.annotation.Size 9 | 10 | private const val MAX_NUM_POINTS = 100 11 | private const val FRACTION_OFFSET = 0 12 | private const val X_OFFSET = 1 13 | private const val Y_OFFSET = 2 14 | private const val NUM_COMPONENTS = 3 15 | 16 | /** 17 | * PathKeyframeSet relies on approximating the Path as a series of line segments. The line segments 18 | * are recursively divided until there is less than 1/2 pixel error between the lines and the curve. 19 | * Each point of the line segment is converted to a [Keyframe] and a linear interpolation 20 | * between keyframes creates a good approximation of the curve. 21 | */ 22 | internal class PathKeyframeSet(path: Path) : KeyframeSet() { 23 | 24 | private val tempPointF = PointF() 25 | private val keyframeData: FloatArray 26 | 27 | override val keyframes: List> = emptyList() 28 | 29 | init { 30 | if (path.isEmpty) { 31 | throw IllegalArgumentException("The path must not be empty") 32 | } 33 | keyframeData = approximate(path, 0.5f) 34 | } 35 | 36 | override fun getAnimatedValue(fraction: Float): PointF { 37 | val numPoints = keyframeData.size / NUM_COMPONENTS 38 | if (fraction < 0) { 39 | return interpolateInRange(fraction, 0, 1) 40 | } 41 | if (fraction > 1) { 42 | return interpolateInRange(fraction, numPoints - 2, numPoints - 1) 43 | } 44 | if (fraction == 0f) { 45 | return pointForIndex(0) 46 | } 47 | if (fraction == 1f) { 48 | return pointForIndex(numPoints - 1) 49 | } 50 | // Binary search for the correct section. 51 | var low = 0 52 | var high = numPoints - 1 53 | while (low <= high) { 54 | val mid = (low + high) / 2 55 | val midFraction = keyframeData[mid * NUM_COMPONENTS + FRACTION_OFFSET] 56 | if (fraction < midFraction) { 57 | high = mid - 1 58 | } else if (fraction > midFraction) { 59 | low = mid + 1 60 | } else { 61 | return pointForIndex(mid) 62 | } 63 | } 64 | // Now high is below the fraction and low is above the fraction. 65 | return interpolateInRange(fraction, high, low) 66 | } 67 | 68 | private fun interpolateInRange(fraction: Float, startIndex: Int, endIndex: Int): PointF { 69 | val startBase = startIndex * NUM_COMPONENTS 70 | val endBase = endIndex * NUM_COMPONENTS 71 | val startFraction = keyframeData[startBase + FRACTION_OFFSET] 72 | val endFraction = keyframeData[endBase + FRACTION_OFFSET] 73 | val intervalFraction = (fraction - startFraction) / (endFraction - startFraction) 74 | val startX = keyframeData[startBase + X_OFFSET] 75 | val endX = keyframeData[endBase + X_OFFSET] 76 | val startY = keyframeData[startBase + Y_OFFSET] 77 | val endY = keyframeData[endBase + Y_OFFSET] 78 | val x = lerp(startX, endX, intervalFraction) 79 | val y = lerp(startY, endY, intervalFraction) 80 | tempPointF.set(x, y) 81 | return tempPointF 82 | } 83 | 84 | private fun pointForIndex(index: Int): PointF { 85 | val base = index * NUM_COMPONENTS 86 | val xOffset = base + X_OFFSET 87 | val yOffset = base + Y_OFFSET 88 | tempPointF.set(keyframeData[xOffset], keyframeData[yOffset]) 89 | return tempPointF 90 | } 91 | } 92 | 93 | /** Implementation of [Path.approximate] for pre-O devices. */ 94 | @Size(multiple = 3) 95 | private fun approximate(path: Path, @FloatRange(from = 0.0) acceptableError: Float): FloatArray { 96 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 97 | return path.approximate(acceptableError) 98 | } 99 | if (acceptableError < 0) { 100 | throw IllegalArgumentException("acceptableError must be greater than or equal to 0") 101 | } 102 | // Measure the total length the whole pathData. 103 | val measureForTotalLength = PathMeasure(path, false) 104 | var totalLength = 0f 105 | // The sum of the previous contour plus the current one. Using the sum here 106 | // because we want to directly subtract from it later. 107 | val summedContourLengths = mutableListOf() 108 | summedContourLengths.add(0f) 109 | do { 110 | val pathLength = measureForTotalLength.length 111 | totalLength += pathLength 112 | summedContourLengths.add(totalLength) 113 | } while (measureForTotalLength.nextContour()) 114 | 115 | // Now determine how many sample points we need, and the step for next sample. 116 | val pathMeasure = PathMeasure(path, false) 117 | 118 | val numPoints = Math.min(MAX_NUM_POINTS, (totalLength / acceptableError).toInt() + 1) 119 | 120 | val coords = FloatArray(NUM_COMPONENTS * numPoints) 121 | val position = FloatArray(2) 122 | 123 | var contourIndex = 0 124 | val step = totalLength / (numPoints - 1) 125 | var cumulativeDistance = 0f 126 | 127 | // For each sample point, determine whether we need to move on to next contour. 128 | // After we find the right contour, then sample it using the current distance value minus 129 | // the previously sampled contours' total length. 130 | for (i in 0 until numPoints) { 131 | // The cumulative distance traveled minus the total length of the previous contours 132 | // (not including the current contour). 133 | val contourDistance = cumulativeDistance - summedContourLengths[contourIndex] 134 | pathMeasure.getPosTan(contourDistance, position, null) 135 | 136 | coords[i * NUM_COMPONENTS + FRACTION_OFFSET] = cumulativeDistance / totalLength 137 | coords[i * NUM_COMPONENTS + X_OFFSET] = position[0] 138 | coords[i * NUM_COMPONENTS + Y_OFFSET] = position[1] 139 | 140 | cumulativeDistance = Math.min(cumulativeDistance + step, totalLength) 141 | 142 | // Using a while statement is necessary in the rare case where step is greater than 143 | // the length a path contour. 144 | while (summedContourLengths[contourIndex + 1] < cumulativeDistance) { 145 | contourIndex++ 146 | pathMeasure.nextContour() 147 | } 148 | } 149 | 150 | coords[(numPoints - 1) * NUM_COMPONENTS + FRACTION_OFFSET] = 1f 151 | return coords 152 | } 153 | 154 | private fun lerp(a: Float, b: Float, @FloatRange(from = 0.0, to = 1.0) t: Float): Float { 155 | return a + (b - a) * t 156 | } 157 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/PathNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Path 4 | 5 | /** A [Node] that paints a path. */ 6 | class PathNode private constructor( 7 | rotation: List>, 8 | pivotX: List>, 9 | pivotY: List>, 10 | scaleX: List>, 11 | scaleY: List>, 12 | translateX: List>, 13 | translateY: List>, 14 | fillColor: List>, 15 | fillColorComplex: ComplexColor?, 16 | fillAlpha: List>, 17 | strokeColor: List>, 18 | strokeColorComplex: ComplexColor?, 19 | strokeAlpha: List>, 20 | strokeWidth: List>, 21 | trimPathStart: List>, 22 | trimPathEnd: List>, 23 | trimPathOffset: List>, 24 | strokeLineCap: StrokeLineCap, 25 | strokeLineJoin: StrokeLineJoin, 26 | strokeMiterLimit: List>, 27 | strokeDashArray: List>, 28 | strokeDashOffset: List>, 29 | fillType: FillType, 30 | isStrokeScaling: Boolean, 31 | private val pathData: List> 32 | ) : RenderNode( 33 | rotation, 34 | pivotX, 35 | pivotY, 36 | scaleX, 37 | scaleY, 38 | translateX, 39 | translateY, 40 | fillColor, 41 | fillColorComplex, 42 | fillAlpha, 43 | strokeColor, 44 | strokeColorComplex, 45 | strokeAlpha, 46 | strokeWidth, 47 | trimPathStart, 48 | trimPathEnd, 49 | trimPathOffset, 50 | strokeLineCap, 51 | strokeLineJoin, 52 | strokeMiterLimit, 53 | strokeDashArray, 54 | strokeDashOffset, 55 | fillType, 56 | isStrokeScaling 57 | ) { 58 | 59 | // 60 | 61 | override fun toLayer(timeline: PropertyTimeline): PathLayer { 62 | return PathLayer(timeline, this) 63 | } 64 | 65 | internal class PathLayer(timeline: PropertyTimeline, node: PathNode) : RenderNode.RenderLayer(timeline, node) { 66 | private val pathData = registerAnimatableProperty(node.pathData) 67 | 68 | override fun onInitPath(outPath: Path) { 69 | PathData.toPath(pathData.animatedValue, outPath) 70 | } 71 | } 72 | 73 | // 74 | 75 | // 76 | 77 | @DslMarker 78 | private annotation class PathNodeMarker 79 | 80 | /** Builder class used to create [PathNode]s. */ 81 | @PathNodeMarker 82 | class Builder internal constructor() : RenderNode.Builder() { 83 | private val pathData = asAnimations(PathData()) 84 | 85 | // Path data. 86 | 87 | fun pathData(initialPathData: String): Builder { 88 | return pathData(PathData.parse(initialPathData)) 89 | } 90 | 91 | fun pathData(initialPathData: PathData): Builder { 92 | return replaceFirstAnimation(pathData, asAnimation(initialPathData)) 93 | } 94 | 95 | @SafeVarargs 96 | fun pathData(vararg animations: Animation<*, PathData>): Builder { 97 | return replaceAnimations(pathData, *animations) 98 | } 99 | 100 | fun pathData(animations: List>): Builder { 101 | return replaceAnimations(pathData, animations) 102 | } 103 | 104 | override val self = this 105 | 106 | override fun build(): PathNode { 107 | return PathNode( 108 | rotation, 109 | pivotX, 110 | pivotY, 111 | scaleX, 112 | scaleY, 113 | translateX, 114 | translateY, 115 | fillColor, 116 | fillColorComplex, 117 | fillAlpha, 118 | strokeColor, 119 | strokeColorComplex, 120 | strokeAlpha, 121 | strokeWidth, 122 | trimPathStart, 123 | trimPathEnd, 124 | trimPathOffset, 125 | strokeLineCap, 126 | strokeLineJoin, 127 | strokeMiterLimit, 128 | strokeDashArray, 129 | strokeDashOffset, 130 | fillType, 131 | isScalingStroke, 132 | pathData 133 | ) 134 | } 135 | } 136 | 137 | // 138 | 139 | companion object { 140 | 141 | @JvmStatic 142 | fun builder(): Builder { 143 | return Builder() 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/Property.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.view.animation.LinearInterpolator 4 | import androidx.annotation.IntRange 5 | import com.github.alexjlockwood.kyrie.Animation.RepeatMode 6 | import java.util.ArrayList 7 | import java.util.Collections 8 | import java.util.Comparator 9 | 10 | internal class Property(animations: List>) { 11 | 12 | private val animations: List> 13 | private val listeners = ArrayList() 14 | private var currentPlayTime: Long = 0 15 | 16 | val totalDuration: Long 17 | 18 | // Iterate backwards through the list and stop at the first 19 | // animation that has a start time less than or equal to the 20 | // current play time. 21 | private val currentAnimation: Animation<*, V> 22 | get() { 23 | // TODO: can this search be faster? 24 | val size = animations.size 25 | val lastAnimation = animations[size - 1] 26 | if (lastAnimation.startDelay <= currentPlayTime) { 27 | return lastAnimation 28 | } 29 | var animation = lastAnimation 30 | for (i in size - 1 downTo 0) { 31 | animation = animations[i] 32 | val startTime = animation.startDelay 33 | if (startTime <= currentPlayTime) { 34 | break 35 | } 36 | } 37 | return animation 38 | } 39 | 40 | val animatedValue: V 41 | get() { 42 | val animation = currentAnimation 43 | return animation.getAnimatedValue(getInterpolatedCurrentAnimationFraction(animation)) 44 | } 45 | 46 | init { 47 | // Sort the animations. 48 | this.animations = ArrayList(animations) 49 | Collections.sort(this.animations, ANIMATION_COMPARATOR) 50 | 51 | // Compute the total duration. 52 | var totalDuration: Long = 0 53 | run { 54 | var i = 0 55 | val size = this.animations.size 56 | while (i < size) { 57 | val currTotalDuration = this.animations[i].totalDuration 58 | if (currTotalDuration == Animation.INFINITE) { 59 | totalDuration = Animation.INFINITE 60 | break 61 | } 62 | totalDuration = Math.max(currTotalDuration, totalDuration) 63 | i++ 64 | } 65 | } 66 | this.totalDuration = totalDuration 67 | 68 | // Fill in any missing start values. 69 | var prevAnimation: Animation<*, V>? = null 70 | var i = 0 71 | val size = this.animations.size 72 | while (i < size) { 73 | val currAnimation = this.animations[i] 74 | if (prevAnimation != null) { 75 | currAnimation.setupStartValue(prevAnimation.getAnimatedValue(1f)) 76 | } 77 | prevAnimation = currAnimation 78 | i++ 79 | } 80 | } 81 | 82 | fun setCurrentPlayTime(@IntRange(from = 0L) currentPlayTime: Long) { 83 | var currentPlayTime = currentPlayTime 84 | if (currentPlayTime < 0) { 85 | currentPlayTime = 0 86 | } else if (totalDuration != Animation.INFINITE && totalDuration < currentPlayTime) { 87 | currentPlayTime = totalDuration 88 | } 89 | if (this.currentPlayTime != currentPlayTime) { 90 | this.currentPlayTime = currentPlayTime 91 | // TODO: optimize this by notifying only when we know the computed value has changed 92 | // TODO: add a computeValue() method or something on Animation? 93 | notifyListeners() 94 | } 95 | } 96 | 97 | fun addListener(listener: Listener) { 98 | listeners.add(listener) 99 | } 100 | 101 | private fun notifyListeners() { 102 | listeners.forEach { it.onCurrentPlayTimeChanged(this) } 103 | } 104 | 105 | /** 106 | * Returns the progress into the current animation between 0 and 1. This does not take into 107 | * account any interpolation that the animation may have. 108 | */ 109 | private fun getLinearCurrentAnimationFraction(animation: Animation<*, V>): Float { 110 | val startTime = animation.startDelay.toFloat() 111 | val duration = animation.duration.toFloat() 112 | if (duration == 0f) { 113 | return 1f 114 | } 115 | val totalDuration = animation.totalDuration 116 | var currentPlayTime = this.currentPlayTime 117 | if (totalDuration != Animation.INFINITE) { 118 | // Don't let the current play time exceed the animation's total duration if it isn't infinite. 119 | currentPlayTime = Math.min(currentPlayTime, totalDuration) 120 | } 121 | val fraction = (currentPlayTime - startTime) / duration 122 | val currentIteration = getCurrentIteration(fraction) 123 | val repeatCount = animation.repeatCount 124 | val repeatMode = animation.repeatMode 125 | var currentFraction = fraction - currentIteration 126 | if (0 < currentIteration 127 | && repeatMode == RepeatMode.REVERSE 128 | && (currentIteration < repeatCount + 1 || repeatCount == Animation.INFINITE)) { 129 | // TODO: when reversing, check if currentIteration % 2 == 0 instead 130 | if (currentIteration % 2 != 0) { 131 | currentFraction = 1 - currentFraction 132 | } 133 | } 134 | return currentFraction 135 | } 136 | 137 | /** 138 | * Takes the value of [.getLinearCurrentAnimationFraction] and interpolates it 139 | * with the current animation's interpolator. 140 | */ 141 | private fun getInterpolatedCurrentAnimationFraction(animation: Animation<*, V>): Float { 142 | var interpolator = animation.interpolator 143 | if (interpolator == null) { 144 | interpolator = DEFAULT_INTERPOLATOR 145 | } 146 | return interpolator.getInterpolation(getLinearCurrentAnimationFraction(animation)) 147 | } 148 | 149 | interface Listener { 150 | fun onCurrentPlayTimeChanged(property: Property<*>) 151 | } 152 | 153 | companion object { 154 | private val DEFAULT_INTERPOLATOR = LinearInterpolator() 155 | private val ANIMATION_COMPARATOR = Comparator> { a1, a2 -> 156 | // Animations with smaller start times are sorted first. 157 | val s1 = a1.startDelay 158 | val s2 = a2.startDelay 159 | if (s1 != s2) { 160 | return@Comparator if (s1 < s2) -1 else 1 161 | } 162 | val d1 = a1.totalDuration 163 | val d2 = a2.totalDuration 164 | if (d1 == Animation.INFINITE || d2 == Animation.INFINITE) { 165 | // Infinite animations are sorted last. 166 | return@Comparator if (d1 == d2) 0 else if (d1 == Animation.INFINITE) 1 else -1 167 | } 168 | // Animations with smaller end times are sorted first. 169 | val e1 = s1 + d1 170 | val e2 = s2 + d2 171 | 172 | if (e1 < e2) -1 else if (e1 > e2) 1 else 0 173 | } 174 | 175 | private fun getCurrentIteration(fraction: Float): Int { 176 | // If the overall fraction is a positive integer, we consider the current iteration to be 177 | // complete. In other words, the fraction for the current iteration would be 1, and the 178 | // current iteration would be overall fraction - 1. 179 | var iteration = Math.floor(fraction.toDouble()).toFloat() 180 | if (fraction == iteration && fraction > 0) { 181 | iteration-- 182 | } 183 | return iteration.toInt() 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/PropertyTimeline.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import androidx.annotation.IntRange 4 | 5 | import java.util.ArrayList 6 | 7 | internal class PropertyTimeline(private val drawable: KyrieDrawable) { 8 | 9 | private val properties = ArrayList>() 10 | private val listener = object : Property.Listener { 11 | override fun onCurrentPlayTimeChanged(property: Property<*>) { 12 | drawable.invalidateSelf() 13 | } 14 | } 15 | 16 | var totalDuration: Long = 0 17 | private set 18 | 19 | fun registerAnimatableProperty(animations: List>): Property { 20 | val property = Property(animations) 21 | properties.add(property) 22 | property.addListener(listener) 23 | if (totalDuration != Animation.INFINITE) { 24 | val currTotalDuration = property.totalDuration 25 | totalDuration = if (currTotalDuration == Animation.INFINITE) { 26 | Animation.INFINITE 27 | } else { 28 | Math.max(currTotalDuration, totalDuration) 29 | } 30 | } 31 | return property 32 | } 33 | 34 | fun setCurrentPlayTime(@IntRange(from = 0) currentPlayTime: Long) { 35 | properties.forEach { it.setCurrentPlayTime(currentPlayTime) } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/RectangleNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Path 4 | import android.graphics.RectF 5 | import androidx.annotation.FloatRange 6 | 7 | /** A [Node] that paints a rectangle. */ 8 | class RectangleNode private constructor( 9 | rotation: List>, 10 | pivotX: List>, 11 | pivotY: List>, 12 | scaleX: List>, 13 | scaleY: List>, 14 | translateX: List>, 15 | translateY: List>, 16 | fillColor: List>, 17 | fillColorComplex: ComplexColor?, 18 | fillAlpha: List>, 19 | strokeColor: List>, 20 | strokeColorComplex: ComplexColor?, 21 | strokeAlpha: List>, 22 | strokeWidth: List>, 23 | trimPathStart: List>, 24 | trimPathEnd: List>, 25 | trimPathOffset: List>, 26 | strokeLineCap: StrokeLineCap, 27 | strokeLineJoin: StrokeLineJoin, 28 | strokeMiterLimit: List>, 29 | strokeDashArray: List>, 30 | strokeDashOffset: List>, 31 | fillType: FillType, 32 | isStrokeScaling: Boolean, 33 | private val x: List>, 34 | private val y: List>, 35 | private val width: List>, 36 | private val height: List>, 37 | private val cornerRadiusX: List>, 38 | private val cornerRadiusY: List> 39 | ) : RenderNode( 40 | rotation, 41 | pivotX, 42 | pivotY, 43 | scaleX, 44 | scaleY, 45 | translateX, 46 | translateY, 47 | fillColor, 48 | fillColorComplex, 49 | fillAlpha, 50 | strokeColor, 51 | strokeColorComplex, 52 | strokeAlpha, 53 | strokeWidth, 54 | trimPathStart, 55 | trimPathEnd, 56 | trimPathOffset, 57 | strokeLineCap, 58 | strokeLineJoin, 59 | strokeMiterLimit, 60 | strokeDashArray, 61 | strokeDashOffset, 62 | fillType, 63 | isStrokeScaling 64 | ) { 65 | 66 | // 67 | 68 | override fun toLayer(timeline: PropertyTimeline): RectangleLayer { 69 | return RectangleLayer(timeline, this) 70 | } 71 | 72 | internal class RectangleLayer(timeline: PropertyTimeline, node: RectangleNode) : RenderNode.RenderLayer(timeline, node) { 73 | private val x = registerAnimatableProperty(node.x) 74 | private val y = registerAnimatableProperty(node.y) 75 | private val width = registerAnimatableProperty(node.width) 76 | private val height = registerAnimatableProperty(node.height) 77 | private val cornerRadiusX = registerAnimatableProperty(node.cornerRadiusX) 78 | private val cornerRadiusY = registerAnimatableProperty(node.cornerRadiusY) 79 | 80 | private val tempRect = RectF() 81 | 82 | override fun onInitPath(outPath: Path) { 83 | val l = x.animatedValue 84 | val t = y.animatedValue 85 | val r = l + width.animatedValue 86 | val b = t + height.animatedValue 87 | val rx = cornerRadiusX.animatedValue 88 | val ry = cornerRadiusY.animatedValue 89 | tempRect.set(l, t, r, b) 90 | outPath.addRoundRect(tempRect, rx, ry, Path.Direction.CW) 91 | } 92 | } 93 | 94 | // 95 | 96 | // 97 | 98 | @DslMarker 99 | private annotation class RectangleNodeMarker 100 | 101 | /** Builder class used to create [RectangleNode]s. */ 102 | @RectangleNodeMarker 103 | class Builder internal constructor() : RenderNode.Builder() { 104 | private val x = asAnimations(0f) 105 | private val y = asAnimations(0f) 106 | private val width = asAnimations(0f) 107 | private val height = asAnimations(0f) 108 | private val cornerRadiusX = asAnimations(0f) 109 | private val cornerRadiusY = asAnimations(0f) 110 | 111 | // X. 112 | 113 | fun x(initialX: Float): Builder { 114 | return replaceFirstAnimation(x, asAnimation(initialX)) 115 | } 116 | 117 | @SafeVarargs 118 | fun x(vararg animations: Animation<*, Float>): Builder { 119 | return replaceAnimations(x, *animations) 120 | } 121 | 122 | fun x(animations: List>): Builder { 123 | return replaceAnimations(x, animations) 124 | } 125 | 126 | // Y. 127 | 128 | fun y(initialY: Float): Builder { 129 | return replaceFirstAnimation(y, asAnimation(initialY)) 130 | } 131 | 132 | @SafeVarargs 133 | fun y(vararg animations: Animation<*, Float>): Builder { 134 | return replaceAnimations(y, *animations) 135 | } 136 | 137 | fun y(animations: List>): Builder { 138 | return replaceAnimations(y, animations) 139 | } 140 | 141 | // Width. 142 | 143 | fun width(@FloatRange(from = 0.0) initialWidth: Float): Builder { 144 | return replaceFirstAnimation(width, asAnimation(initialWidth)) 145 | } 146 | 147 | @SafeVarargs 148 | fun width(vararg animations: Animation<*, Float>): Builder { 149 | return replaceAnimations(width, *animations) 150 | } 151 | 152 | fun width(animations: List>): Builder { 153 | return replaceAnimations(width, animations) 154 | } 155 | 156 | // Height. 157 | 158 | fun height(@FloatRange(from = 0.0) initialHeight: Float): Builder { 159 | return replaceFirstAnimation(height, asAnimation(initialHeight)) 160 | } 161 | 162 | @SafeVarargs 163 | fun height(vararg animations: Animation<*, Float>): Builder { 164 | return replaceAnimations(height, *animations) 165 | } 166 | 167 | fun height(animations: List>): Builder { 168 | return replaceAnimations(height, animations) 169 | } 170 | 171 | // Corner radius X. 172 | 173 | fun cornerRadiusX(@FloatRange(from = 0.0) initialCornerRadiusX: Float): Builder { 174 | return replaceFirstAnimation(cornerRadiusX, asAnimation(initialCornerRadiusX)) 175 | } 176 | 177 | @SafeVarargs 178 | fun cornerRadiusX(vararg animations: Animation<*, Float>): Builder { 179 | return replaceAnimations(cornerRadiusX, *animations) 180 | } 181 | 182 | fun cornerRadiusX(animations: List>): Builder { 183 | return replaceAnimations(cornerRadiusX, animations) 184 | } 185 | 186 | // Corner radius Y. 187 | 188 | fun cornerRadiusY(@FloatRange(from = 0.0) initialCornerRadiusY: Float): Builder { 189 | return replaceFirstAnimation(cornerRadiusY, asAnimation(initialCornerRadiusY)) 190 | } 191 | 192 | @SafeVarargs 193 | fun cornerRadiusY(vararg animations: Animation<*, Float>): Builder { 194 | return replaceAnimations(cornerRadiusY, *animations) 195 | } 196 | 197 | fun cornerRadiusY(animations: List>): Builder { 198 | return replaceAnimations(cornerRadiusY, animations) 199 | } 200 | 201 | override val self = this 202 | 203 | override fun build(): RectangleNode { 204 | return RectangleNode( 205 | rotation, 206 | pivotX, 207 | pivotY, 208 | scaleX, 209 | scaleY, 210 | translateX, 211 | translateY, 212 | fillColor, 213 | fillColorComplex, 214 | fillAlpha, 215 | strokeColor, 216 | strokeColorComplex, 217 | strokeAlpha, 218 | strokeWidth, 219 | trimPathStart, 220 | trimPathEnd, 221 | trimPathOffset, 222 | strokeLineCap, 223 | strokeLineJoin, 224 | strokeMiterLimit, 225 | strokeDashArray, 226 | strokeDashOffset, 227 | fillType, 228 | isScalingStroke, 229 | x, 230 | y, 231 | width, 232 | height, 233 | cornerRadiusX, 234 | cornerRadiusY 235 | ) 236 | } 237 | } 238 | 239 | // 240 | 241 | companion object { 242 | 243 | @JvmStatic 244 | fun builder(): Builder { 245 | return Builder() 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/StrokeLineCap.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | /** Stroke line cap determines the shape that should be used at the corners of stroked paths. */ 4 | enum class StrokeLineCap { 5 | /** A butt stroke line cap. */ 6 | BUTT, 7 | /** A round stroke line cap. */ 8 | ROUND, 9 | /** A square stroke line cap. */ 10 | SQUARE 11 | } 12 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/StrokeLineJoin.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | /** Stroke line join determines the shape that should be used at the ends of a stroked sub-path. */ 4 | enum class StrokeLineJoin { 5 | /** A miter stroke line join. */ 6 | MITER, 7 | /** A round stroke line join. */ 8 | ROUND, 9 | /** A bevel stroke line join. */ 10 | BEVEL 11 | } 12 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/Styleable.java: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie; 2 | 3 | import androidx.annotation.StyleableRes; 4 | 5 | final class Styleable { 6 | 7 | interface Vector { 8 | @StyleableRes int NAME = 0; 9 | @StyleableRes int TINT = 1; 10 | @StyleableRes int HEIGHT = 2; 11 | @StyleableRes int WIDTH = 3; 12 | @StyleableRes int ALPHA = 4; 13 | @StyleableRes int AUTO_MIRRORED = 5; 14 | @StyleableRes int TINT_MODE = 6; 15 | @StyleableRes int VIEWPORT_WIDTH = 7; 16 | @StyleableRes int VIEWPORT_HEIGHT = 8; 17 | } 18 | 19 | @StyleableRes 20 | static final int[] VECTOR = { 21 | android.R.attr.name, 22 | android.R.attr.tint, 23 | android.R.attr.height, 24 | android.R.attr.width, 25 | android.R.attr.alpha, 26 | android.R.attr.autoMirrored, 27 | android.R.attr.tintMode, 28 | android.R.attr.viewportWidth, 29 | android.R.attr.viewportHeight, 30 | }; 31 | 32 | interface Group { 33 | @StyleableRes int NAME = 0; 34 | @StyleableRes int PIVOT_X = 1; 35 | @StyleableRes int PIVOT_Y = 2; 36 | @StyleableRes int SCALE_X = 3; 37 | @StyleableRes int SCALE_Y = 4; 38 | @StyleableRes int ROTATION = 5; 39 | @StyleableRes int TRANSLATE_X = 6; 40 | @StyleableRes int TRANSLATE_Y = 7; 41 | } 42 | 43 | @StyleableRes 44 | static final int[] GROUP = { 45 | android.R.attr.name, 46 | android.R.attr.pivotX, 47 | android.R.attr.pivotY, 48 | android.R.attr.scaleX, 49 | android.R.attr.scaleY, 50 | android.R.attr.rotation, 51 | android.R.attr.translateX, 52 | android.R.attr.translateY, 53 | }; 54 | 55 | interface Path { 56 | @StyleableRes int NAME = 0; 57 | @StyleableRes int FILL_COLOR = 1; 58 | @StyleableRes int PATH_DATA = 2; 59 | @StyleableRes int STROKE_COLOR = 3; 60 | @StyleableRes int STROKE_WIDTH = 4; 61 | @StyleableRes int TRIM_PATH_START = 5; 62 | @StyleableRes int TRIM_PATH_END = 6; 63 | @StyleableRes int TRIM_PATH_OFFSET = 7; 64 | @StyleableRes int STROKE_LINE_CAP = 8; 65 | @StyleableRes int STROKE_LINE_JOIN = 9; 66 | @StyleableRes int STROKE_MITER_LIMIT = 10; 67 | @StyleableRes int STROKE_ALPHA = 11; 68 | @StyleableRes int FILL_ALPHA = 12; 69 | @StyleableRes int FILL_TYPE = 13; 70 | } 71 | 72 | @StyleableRes 73 | static final int[] PATH = { 74 | android.R.attr.name, 75 | android.R.attr.fillColor, 76 | android.R.attr.pathData, 77 | android.R.attr.strokeColor, 78 | android.R.attr.strokeWidth, 79 | android.R.attr.trimPathStart, 80 | android.R.attr.trimPathEnd, 81 | android.R.attr.trimPathOffset, 82 | android.R.attr.strokeLineCap, 83 | android.R.attr.strokeLineJoin, 84 | android.R.attr.strokeMiterLimit, 85 | android.R.attr.strokeAlpha, 86 | android.R.attr.fillAlpha, 87 | android.R.attr.fillType, 88 | }; 89 | 90 | interface ClipPath { 91 | @StyleableRes int NAME = 0; 92 | @StyleableRes int PATH_DATA = 1; 93 | @StyleableRes int FILL_TYPE = 2; 94 | } 95 | 96 | @StyleableRes static final int[] CLIP_PATH = { 97 | android.R.attr.name, 98 | android.R.attr.pathData, 99 | android.R.attr.fillType 100 | }; 101 | 102 | interface AnimatedVector { 103 | @StyleableRes int DRAWABLE = 0; 104 | } 105 | 106 | @StyleableRes static final int[] ANIMATED_VECTOR = {android.R.attr.drawable}; 107 | 108 | interface Target { 109 | @StyleableRes int NAME = 0; 110 | @StyleableRes int ANIMATION = 1; 111 | } 112 | 113 | @StyleableRes static final int[] TARGET = {android.R.attr.name, android.R.attr.animation}; 114 | 115 | interface AnimatorSet { 116 | @StyleableRes int ORDERING = 0; 117 | } 118 | 119 | @StyleableRes static final int[] ANIMATOR_SET = {android.R.attr.ordering}; 120 | 121 | interface Animator { 122 | @StyleableRes int INTERPOLATOR = 0; 123 | @StyleableRes int DURATION = 1; 124 | @StyleableRes int START_OFFSET = 2; 125 | @StyleableRes int REPEAT_COUNT = 3; 126 | @StyleableRes int REPEAT_MODE = 4; 127 | @StyleableRes int VALUE_FROM = 5; 128 | @StyleableRes int VALUE_TO = 6; 129 | @StyleableRes int VALUE_TYPE = 7; 130 | } 131 | 132 | @StyleableRes 133 | static final int[] ANIMATOR = { 134 | android.R.attr.interpolator, 135 | android.R.attr.duration, 136 | android.R.attr.startOffset, 137 | android.R.attr.repeatCount, 138 | android.R.attr.repeatMode, 139 | android.R.attr.valueFrom, 140 | android.R.attr.valueTo, 141 | android.R.attr.valueType, 142 | }; 143 | 144 | interface PropertyAnimator { 145 | @StyleableRes int PROPERTY_NAME = 0; 146 | @StyleableRes int PATH_DATA = 1; 147 | @StyleableRes int PROPERTY_X_NAME = 2; 148 | @StyleableRes int PROPERTY_Y_NAME = 3; 149 | } 150 | 151 | @StyleableRes 152 | static final int[] PROPERTY_ANIMATOR = { 153 | android.R.attr.propertyName, 154 | android.R.attr.pathData, 155 | android.R.attr.propertyXName, 156 | android.R.attr.propertyYName, 157 | }; 158 | 159 | interface PropertyValuesHolder { 160 | @StyleableRes int VALUE_FROM = 0; 161 | @StyleableRes int VALUE_TO = 1; 162 | @StyleableRes int VALUE_TYPE = 2; 163 | @StyleableRes int PROPERTY_NAME = 3; 164 | } 165 | 166 | @StyleableRes 167 | static final int[] PROPERTY_VALUES_HOLDER = { 168 | android.R.attr.valueFrom, 169 | android.R.attr.valueTo, 170 | android.R.attr.valueType, 171 | android.R.attr.propertyName, 172 | }; 173 | 174 | interface Keyframe { 175 | @StyleableRes int VALUE = 0; 176 | @StyleableRes int INTERPOLATOR = 1; 177 | @StyleableRes int VALUE_TYPE = 2; 178 | @StyleableRes int FRACTION = 3; 179 | } 180 | 181 | @StyleableRes 182 | static final int[] KEYFRAME = { 183 | android.R.attr.value, 184 | android.R.attr.interpolator, 185 | android.R.attr.valueType, 186 | android.R.attr.fraction, 187 | }; 188 | 189 | interface PathInterpolator { 190 | @StyleableRes int CONTROL_X1 = 0; 191 | @StyleableRes int CONTROL_Y1 = 1; 192 | @StyleableRes int CONTROL_X2 = 2; 193 | @StyleableRes int CONTROL_Y2 = 3; 194 | @StyleableRes int PATH_DATA = 4; 195 | } 196 | 197 | @StyleableRes 198 | static final int[] PATH_INTERPOLATOR = { 199 | android.R.attr.controlX1, 200 | android.R.attr.controlY1, 201 | android.R.attr.controlX2, 202 | android.R.attr.controlY2, 203 | android.R.attr.pathData, 204 | }; 205 | 206 | private Styleable() {} 207 | } 208 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/TransformNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Matrix 5 | import android.graphics.PointF 6 | import androidx.annotation.Size 7 | 8 | /** Abstract base [Node] for all node types that can be transformed. */ 9 | abstract class TransformNode internal constructor( 10 | val rotation: List>, 11 | val pivotX: List>, 12 | val pivotY: List>, 13 | val scaleX: List>, 14 | val scaleY: List>, 15 | val translateX: List>, 16 | val translateY: List> 17 | ) : Node() { 18 | 19 | // 20 | 21 | abstract override fun toLayer(timeline: PropertyTimeline): TransformLayer 22 | 23 | internal abstract class TransformLayer(private val timeline: PropertyTimeline, node: TransformNode) : Layer { 24 | private val rotation = registerAnimatableProperty(node.rotation) 25 | private val pivotX = registerAnimatableProperty(node.pivotX) 26 | private val pivotY = registerAnimatableProperty(node.pivotY) 27 | private val scaleX = registerAnimatableProperty(node.scaleX) 28 | private val scaleY = registerAnimatableProperty(node.scaleY) 29 | private val translateX = registerAnimatableProperty(node.translateX) 30 | private val translateY = registerAnimatableProperty(node.translateY) 31 | 32 | private val tempMatrix = Matrix() 33 | 34 | @Size(value = 4) 35 | private val tempUnitVectors = FloatArray(4) 36 | 37 | fun registerAnimatableProperty(animations: List>): Property { 38 | return timeline.registerAnimatableProperty(animations) 39 | } 40 | 41 | override fun draw(canvas: Canvas, parentMatrix: Matrix, viewportScale: PointF) { 42 | val rotation = this.rotation.animatedValue 43 | val pivotX = this.pivotX.animatedValue 44 | val pivotY = this.pivotY.animatedValue 45 | val scaleX = this.scaleX.animatedValue 46 | val scaleY = this.scaleY.animatedValue 47 | val translateX = this.translateX.animatedValue 48 | val translateY = this.translateY.animatedValue 49 | tempMatrix.set(parentMatrix) 50 | if (translateX + pivotX != 0f || translateY + pivotY != 0f) { 51 | tempMatrix.preTranslate(translateX + pivotX, translateY + pivotY) 52 | } 53 | if (rotation != 0f) { 54 | tempMatrix.preRotate(rotation, 0f, 0f) 55 | } 56 | if (scaleX != 1f || scaleY != 1f) { 57 | tempMatrix.preScale(scaleX, scaleY) 58 | } 59 | if (pivotX != 0f || pivotY != 0f) { 60 | tempMatrix.preTranslate(-pivotX, -pivotY) 61 | } 62 | onDraw(canvas, tempMatrix, viewportScale) 63 | } 64 | 65 | fun getMatrixScale(matrix: Matrix): Float { 66 | // Given unit vectors A = (0, 1) and B = (1, 0). 67 | // After matrix mapping, we got A' and B'. Let theta = the angle b/t A' and B'. 68 | // Therefore, the final scale we want is min(|A'| * sin(theta), |B'| * sin(theta)), 69 | // which is (|A'| * |B'| * sin(theta)) / max (|A'|, |B'|); 70 | // If max (|A'|, |B'|) = 0, that means either x or y has a scale of 0. 71 | // For non-skew case, which is most of the cases, matrix scale is computing exactly the 72 | // scale on x and y axis, and take the minimal of these two. 73 | // For skew case, an unit square will mapped to a parallelogram. And this function will 74 | // return the minimal height of the 2 bases. 75 | val unitVectors = tempUnitVectors 76 | unitVectors[0] = 0f 77 | unitVectors[1] = 1f 78 | unitVectors[2] = 1f 79 | unitVectors[3] = 0f 80 | matrix.mapVectors(unitVectors) 81 | val scaleX = Math.hypot(unitVectors[0].toDouble(), unitVectors[1].toDouble()).toFloat() 82 | val scaleY = Math.hypot(unitVectors[2].toDouble(), unitVectors[3].toDouble()).toFloat() 83 | val crossProduct = cross(unitVectors[0], unitVectors[1], unitVectors[2], unitVectors[3]) 84 | val maxScale = Math.max(scaleX, scaleY) 85 | return if (maxScale > 0) Math.abs(crossProduct) / maxScale else 0f 86 | } 87 | 88 | private fun cross(v1x: Float, v1y: Float, v2x: Float, v2y: Float): Float { 89 | return v1x * v2y - v1y * v2x 90 | } 91 | } 92 | 93 | // 94 | 95 | // 96 | 97 | @DslMarker 98 | private annotation class TransformNodeMarker 99 | 100 | @TransformNodeMarker 101 | abstract class Builder> internal constructor() : Node.Builder() { 102 | val rotation = asAnimations(0f) 103 | val pivotX = asAnimations(0f) 104 | val pivotY = asAnimations(0f) 105 | val scaleX = asAnimations(1f) 106 | val scaleY = asAnimations(1f) 107 | val translateX = asAnimations(0f) 108 | val translateY = asAnimations(0f) 109 | 110 | // Rotation. 111 | 112 | fun rotation(initialRotation: Float): B { 113 | return replaceFirstAnimation(rotation, asAnimation(initialRotation)) 114 | } 115 | 116 | @SafeVarargs 117 | fun rotation(vararg animations: Animation<*, Float>): B { 118 | return replaceAnimations(rotation, *animations) 119 | } 120 | 121 | fun rotation(animations: List>): B { 122 | return replaceAnimations(rotation, animations) 123 | } 124 | 125 | // Pivot X. 126 | 127 | fun pivotX(initialPivotX: Float): B { 128 | return replaceFirstAnimation(pivotX, asAnimation(initialPivotX)) 129 | } 130 | 131 | @SafeVarargs 132 | fun pivotX(vararg animations: Animation<*, Float>): B { 133 | return replaceAnimations(pivotX, *animations) 134 | } 135 | 136 | fun pivotX(animations: List>): B { 137 | return replaceAnimations(pivotX, animations) 138 | } 139 | 140 | // Pivot Y. 141 | 142 | fun pivotY(initialPivotY: Float): B { 143 | return replaceFirstAnimation(pivotY, asAnimation(initialPivotY)) 144 | } 145 | 146 | @SafeVarargs 147 | fun pivotY(vararg animations: Animation<*, Float>): B { 148 | return replaceAnimations(pivotY, *animations) 149 | } 150 | 151 | fun pivotY(animations: List>): B { 152 | return replaceAnimations(pivotY, animations) 153 | } 154 | 155 | // Scale X. 156 | 157 | fun scaleX(initialScaleX: Float): B { 158 | return replaceFirstAnimation(scaleX, asAnimation(initialScaleX)) 159 | } 160 | 161 | @SafeVarargs 162 | fun scaleX(vararg animations: Animation<*, Float>): B { 163 | return replaceAnimations(scaleX, *animations) 164 | } 165 | 166 | fun scaleX(animations: List>): B { 167 | return replaceAnimations(scaleX, animations) 168 | } 169 | 170 | // Scale Y. 171 | 172 | fun scaleY(initialScaleY: Float): B { 173 | return replaceFirstAnimation(scaleY, asAnimation(initialScaleY)) 174 | } 175 | 176 | @SafeVarargs 177 | fun scaleY(vararg animations: Animation<*, Float>): B { 178 | return replaceAnimations(scaleY, *animations) 179 | } 180 | 181 | fun scaleY(animations: List>): B { 182 | return replaceAnimations(scaleY, animations) 183 | } 184 | 185 | // Translate X. 186 | 187 | fun translateX(initialTranslateX: Float): B { 188 | return replaceFirstAnimation(translateX, asAnimation(initialTranslateX)) 189 | } 190 | 191 | @SafeVarargs 192 | fun translateX(vararg animations: Animation<*, Float>): B { 193 | return replaceAnimations(translateX, *animations) 194 | } 195 | 196 | fun translateX(animations: List>): B { 197 | return replaceAnimations(translateX, animations) 198 | } 199 | 200 | // Translate Y. 201 | 202 | fun translateY(initialTranslateY: Float): B { 203 | return replaceFirstAnimation(translateY, asAnimation(initialTranslateY)) 204 | } 205 | 206 | @SafeVarargs 207 | fun translateY(vararg animations: Animation<*, Float>): B { 208 | return replaceAnimations(translateY, *animations) 209 | } 210 | 211 | fun translateY(animations: List>): B { 212 | return replaceAnimations(translateY, animations) 213 | } 214 | 215 | abstract override fun build(): TransformNode 216 | } 217 | 218 | // 219 | } 220 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/TypedArrayUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.alexjlockwood.kyrie 2 | 3 | import android.content.Context 4 | import android.content.res.ColorStateList 5 | import android.content.res.Resources 6 | import android.content.res.TypedArray 7 | import android.util.AttributeSet 8 | import android.util.TypedValue 9 | import androidx.annotation.AnyRes 10 | import androidx.annotation.ColorInt 11 | import androidx.annotation.StyleableRes 12 | import androidx.appcompat.content.res.AppCompatResources 13 | import org.xmlpull.v1.XmlPullParser 14 | 15 | /** 16 | * Compat methods for accessing TypedArray values. 17 | * 18 | * All the `getNamed*()` functions added the attribute name match, to take care of potential ID 19 | * collision between the private attributes in older OS version (OEM) and the attributes existed in 20 | * the newer OS version. For example, if an private attribute named `"abcdefg"` in Kitkat has the same 21 | * id value as `android:pathData` in Lollipop, we need to match the attribute's name first. 22 | */ 23 | internal object TypedArrayUtils { 24 | 25 | private const val NAMESPACE = "http://schemas.android.com/apk/res/android" 26 | 27 | /** 28 | * @return Whether the current node of the [XmlPullParser] has an attribute with the 29 | * specified `attrName`. 30 | */ 31 | @JvmStatic 32 | fun hasAttribute(parser: XmlPullParser, attrName: String): Boolean { 33 | return parser.getAttributeValue(NAMESPACE, attrName) != null 34 | } 35 | 36 | /** 37 | * Retrieves a float attribute value. In addition to the styleable resource ID, we also make sure 38 | * that the attribute name matches. 39 | * 40 | * @return a float value in the [TypedArray] with the specified `resId`, or `defaultValue` if it does not exist. 41 | */ 42 | @JvmStatic 43 | fun getNamedFloat( 44 | a: TypedArray, 45 | parser: XmlPullParser, 46 | attrName: String, 47 | @StyleableRes resId: Int, 48 | defaultValue: Float 49 | ): Float { 50 | val hasAttr = hasAttribute(parser, attrName) 51 | return if (!hasAttr) { 52 | defaultValue 53 | } else { 54 | a.getFloat(resId, defaultValue) 55 | } 56 | } 57 | 58 | /** 59 | * Retrieves a boolean attribute value. In addition to the styleable resource ID, we also make 60 | * sure that the attribute name matches. 61 | * 62 | * @return a boolean value in the [TypedArray] with the specified `resId`, or `defaultValue` if it does not exist. 63 | */ 64 | @JvmStatic 65 | fun getNamedBoolean( 66 | a: TypedArray, 67 | parser: XmlPullParser, 68 | attrName: String, 69 | @StyleableRes resId: Int, 70 | defaultValue: Boolean 71 | ): Boolean { 72 | val hasAttr = hasAttribute(parser, attrName) 73 | return if (!hasAttr) { 74 | defaultValue 75 | } else { 76 | a.getBoolean(resId, defaultValue) 77 | } 78 | } 79 | 80 | /** 81 | * Retrieves an int attribute value. In addition to the styleable resource ID, we also make sure 82 | * that the attribute name matches. 83 | * 84 | * @return an int value in the [TypedArray] with the specified `resId`, or `defaultValue` if it does not exist. 85 | */ 86 | @JvmStatic 87 | fun getNamedInt( 88 | a: TypedArray, 89 | parser: XmlPullParser, 90 | attrName: String, 91 | @StyleableRes resId: Int, 92 | defaultValue: Int 93 | ): Int { 94 | val hasAttr = hasAttribute(parser, attrName) 95 | return if (!hasAttr) { 96 | defaultValue 97 | } else { 98 | a.getInt(resId, defaultValue) 99 | } 100 | } 101 | 102 | /** 103 | * Retrieves a color attribute value. In addition to the styleable resource ID, we also make sure 104 | * that the attribute name matches. 105 | * 106 | * @return a color value in the [TypedArray] with the specified `resId`, or `defaultValue` if it does not exist. 107 | */ 108 | @JvmStatic 109 | @ColorInt 110 | fun getNamedColor( 111 | a: TypedArray, 112 | parser: XmlPullParser, 113 | attrName: String, 114 | @StyleableRes resId: Int, 115 | @ColorInt defaultValue: Int 116 | ): Int { 117 | val hasAttr = hasAttribute(parser, attrName) 118 | return if (!hasAttr) { 119 | defaultValue 120 | } else { 121 | a.getColor(resId, defaultValue) 122 | } 123 | } 124 | 125 | /** 126 | * Retrieves a complex color attribute value. In addition to the styleable resource ID, we also 127 | * make sure that the attribute name matches. 128 | * 129 | * @return a complex color value form the [TypedArray] with the specified `resId`, or 130 | * containing `defaultValue` if it does not exist. 131 | */ 132 | @JvmStatic 133 | fun getNamedComplexColor( 134 | a: TypedArray, 135 | parser: XmlPullParser, 136 | context: Context, 137 | attrName: String, 138 | @StyleableRes resId: Int, 139 | @ColorInt defaultValue: Int 140 | ): ComplexColor { 141 | if (hasAttribute(parser, attrName)) { 142 | // first check if is a simple color 143 | val value = TypedValue() 144 | a.getValue(resId, value) 145 | if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { 146 | return ComplexColor.from(value.data) 147 | } 148 | 149 | // not a simple color, attempt to inflate complex types 150 | val complexColor = ComplexColor.inflate(context, a.getResourceId(resId, 0)) 151 | if (complexColor != null) return complexColor 152 | } 153 | return ComplexColor.from(defaultValue) 154 | } 155 | 156 | /** 157 | * Retrieves a color state list object. In addition to the styleable resource ID, we also make 158 | * sure that the attribute name matches. 159 | * 160 | * @return a color state list object form the [TypedArray] with the specified `resId`, 161 | * or null if it does not exist. 162 | */ 163 | @JvmStatic 164 | fun getNamedColorStateList( 165 | a: TypedArray, 166 | parser: XmlPullParser, 167 | context: Context, 168 | attrName: String, 169 | @StyleableRes resId: Int 170 | ): ColorStateList? { 171 | if (hasAttribute(parser, attrName)) { 172 | val value = TypedValue() 173 | a.getValue(resId, value) 174 | if (value.type == TypedValue.TYPE_ATTRIBUTE) { 175 | throw UnsupportedOperationException( 176 | "Failed to resolve attribute at index $resId: $value") 177 | } else if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { 178 | // Handle inline color definitions. 179 | return getNamedColorStateListFromInt(value) 180 | } 181 | return AppCompatResources.getColorStateList(context, a.getResourceId(resId, 0)) 182 | } 183 | return null 184 | } 185 | 186 | private fun getNamedColorStateListFromInt(value: TypedValue): ColorStateList { 187 | // This is copied from ResourcesImpl#getNamedColorStateListFromInt in the platform, but the 188 | // ComplexColor caching mechanism has been removed. The practical implication of this is 189 | // minimal, since platform caching is only used by Zygote-preloaded resources. 190 | return ColorStateList.valueOf(value.data) 191 | } 192 | 193 | /** 194 | * Retrieves a resource ID attribute value. In addition to the styleable resource ID, we also make 195 | * sure that the attribute name matches. 196 | * 197 | * @return a resource ID value in the [TypedArray] with the specified `resId`, or 198 | * `defaultValue` if it does not exist. 199 | */ 200 | @JvmStatic 201 | @AnyRes 202 | fun getNamedResourceId( 203 | a: TypedArray, 204 | parser: XmlPullParser, 205 | attrName: String, 206 | @StyleableRes resId: Int, 207 | @AnyRes defaultValue: Int 208 | ): Int { 209 | val hasAttr = hasAttribute(parser, attrName) 210 | return if (!hasAttr) { 211 | defaultValue 212 | } else { 213 | a.getResourceId(resId, defaultValue) 214 | } 215 | } 216 | 217 | /** 218 | * Retrieves a string attribute value. In addition to the styleable resource ID, we also make sure 219 | * that the attribute name matches. 220 | * 221 | * @return a string value in the [TypedArray] with the specified `resId`, or null if 222 | * it does not exist. 223 | */ 224 | @JvmStatic 225 | fun getNamedString( 226 | a: TypedArray, 227 | parser: XmlPullParser, 228 | attrName: String, 229 | @StyleableRes resId: Int 230 | ): String? { 231 | val hasAttr = hasAttribute(parser, attrName) 232 | return if (!hasAttr) { 233 | null 234 | } else { 235 | a.getString(resId) 236 | } 237 | } 238 | 239 | /** 240 | * Retrieve the raw TypedValue for the attribute at index and return a temporary object 241 | * holding its data. This object is only valid until the next call on to [TypedArray]. 242 | */ 243 | @JvmStatic 244 | fun peekNamedValue(a: TypedArray, parser: XmlPullParser, attrName: String, resId: Int): TypedValue? { 245 | val hasAttr = hasAttribute(parser, attrName) 246 | return if (!hasAttr) { 247 | null 248 | } else { 249 | a.peekValue(resId) 250 | } 251 | } 252 | 253 | /** 254 | * Obtains styled attributes from the theme, if available, or unstyled resources if the theme is 255 | * null. 256 | */ 257 | @JvmStatic 258 | fun obtainAttributes( 259 | res: Resources, 260 | theme: Resources.Theme?, 261 | set: AttributeSet, 262 | attrs: IntArray 263 | ): TypedArray { 264 | return if (theme == null) { 265 | res.obtainAttributes(set, attrs) 266 | } else theme.obtainStyledAttributes(set, attrs, 0, 0) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /kyrie/src/main/java/com/github/alexjlockwood/kyrie/package-info.java: -------------------------------------------------------------------------------- 1 | @javax.annotation.ParametersAreNonnullByDefault 2 | package com.github.alexjlockwood.kyrie; 3 | 4 | // TODO: support animatable gradients? 5 | // TODO: support text layers? 6 | // TODO: support image layers? 7 | // TODO: avoid using canvas.clipPath (no anti-alias support)? 8 | // TODO: don't bother starting the animator if there are no keyframes 9 | // TODO: allow clients to pass in string paths to keyframes (instead of PathData objects) 10 | // TODO: possibly change PathMorphKeyframeAnimation to take strings instead of PathData objects 11 | // TODO: support odd length stroke dash array 12 | // TODO: add convenience methods to builders (i.e. cornerRadius, bounds, viewport etc.) 13 | // TODO: auto-make paths morphable 14 | // TODO: add more path effects (i.e. path dash path effect)? 15 | // TODO: set the default pivot x/y values to be the center of the node? 16 | // TODO: add color getInterpolator helpers (similar to d3?) 17 | // TODO: add 'children' methods to the node builders 18 | // TODO: allow null start values for PVH and Keyframe (and then infer their values) 19 | // TODO: rename 'x/y' property to 'left/top' in RectangleNode? 20 | // TODO: double check for copy/paste errors in the builders/nodes/layers 21 | // TODO: reuse paint/other objects more diligently across layers? 22 | // TODO: make it impossible to add 'transform' wrappers to keyframes over and over and over 23 | // TODO: make all strings/pathdata args non null? 24 | // TODO: make it possible to pass Keyframe to translate(), scale(), etc. 25 | // TODO: create more examples, add documentation, add README.md (explain minSdkVersion 14) 26 | // TODO: make it possible to specify resource IDs etc. inside the builders? 27 | // TODO: add support for SVG's preserveAspectRatio attribute 28 | // TODO: make API as small as possible 29 | // TODO: create cache for frequently used objs (paths, paints, etc.) 30 | // TODO: support trimming clip paths? 31 | // TODO: support stroked clip paths? 32 | // TODO: think more about how each node builder has two overloaded methods per property 33 | // TODO: support setting playback speed? 34 | // TODO: allow user to inflate from xml resource as well as drawable resource? 35 | // TODO: support playing animation in reverse? 36 | // TODO: avoid using bitmap internally (encourage view software rendering instead) 37 | // TODO: test inflating multi-file AVDs 38 | // TODO: create kyrie view? 39 | // TODO: make it clear what stuff shouldn't change after the kyrie drawable has been created!!!!!!!! 40 | // TODO: customize behavior when ValueAnimator#areAnimatorsEnabled returns true 41 | // TODO: make sure it works with AnimatedStateListDrawable? 42 | // TODO: avoid setting initial state and animations on nodes separately? combine them somehow? 43 | // TODO: should we use "startOffset" or "startDelay" as terminology? AVD object animators use startOffset 44 | // TODO: should we use linear or accelerate/decelerate as the default interpolator 45 | // TODO: publish sample app on play store? 46 | // TODO: support gradients w/ color state lists? 47 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion rootProject.ext.targetSdkVersion 7 | 8 | defaultConfig { 9 | applicationId "com.example.kyrie" 10 | minSdkVersion rootProject.ext.minSdkVersion 11 | targetSdkVersion rootProject.ext.targetSdkVersion 12 | versionCode 1 13 | versionName "1.0" 14 | vectorDrawables.useSupportLibrary = true 15 | } 16 | buildTypes { 17 | debug { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | lintOptions { 23 | abortOnError false 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation project(':kyrie') 33 | implementation 'androidx.appcompat:appcompat:1.1.0-alpha05' 34 | implementation 'androidx.fragment:fragment:1.1.0-alpha09' 35 | implementation 'androidx.recyclerview:recyclerview:1.0.0' 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 37 | } 38 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sample/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /sample/src/main/java/com/example/kyrie/DemoListFragment.kt: -------------------------------------------------------------------------------- 1 | package com.example.kyrie 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.TextView 9 | import androidx.fragment.app.Fragment 10 | import androidx.recyclerview.widget.RecyclerView 11 | 12 | private val DEMOS = arrayOf( 13 | DemoListFragment.Demo("Polygons", PolygonsFragment::class.java.name), 14 | DemoListFragment.Demo("Progress bars", ProgressFragment::class.java.name), 15 | DemoListFragment.Demo("Path morphing", PathMorphFragment::class.java.name), 16 | DemoListFragment.Demo("Heartbreak", HeartbreakFragment::class.java.name) 17 | ) 18 | 19 | class DemoListFragment : Fragment() { 20 | 21 | private lateinit var callbacks: Callbacks 22 | 23 | override fun onAttach(context: Context) { 24 | super.onAttach(context) 25 | if (context !is Callbacks) { 26 | throw IllegalArgumentException("Host must implement Callbacks interface") 27 | } 28 | callbacks = context 29 | } 30 | 31 | override fun onCreateView( 32 | inflater: LayoutInflater, 33 | container: ViewGroup?, 34 | savedInstanceState: Bundle?): View? { 35 | val view = inflater.inflate(R.layout.fragment_demo_list, container, false) 36 | val recyclerView = view.findViewById(R.id.demo_list) 37 | recyclerView.adapter = Adapter() 38 | return view 39 | } 40 | 41 | private inner class Adapter : RecyclerView.Adapter() { 42 | 43 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 44 | val inflater = LayoutInflater.from(parent.context) 45 | return ViewHolder(inflater.inflate(R.layout.fragment_demo_item, parent, false)) 46 | } 47 | 48 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 49 | holder.bind(DEMOS[position]) 50 | } 51 | 52 | override fun getItemCount(): Int { 53 | return DEMOS.size 54 | } 55 | } 56 | 57 | private inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { 58 | private val textView: TextView 59 | private var demo: Demo? = null 60 | 61 | init { 62 | itemView.setOnClickListener(this) 63 | textView = itemView.findViewById(R.id.demo_text) 64 | } 65 | 66 | fun bind(d: Demo) { 67 | demo = d 68 | textView.text = d.title 69 | } 70 | 71 | override fun onClick(v: View) { 72 | callbacks.onListItemClick(demo!!) 73 | } 74 | } 75 | 76 | data class Demo(val title: String, val fragmentClassName: String) 77 | 78 | interface Callbacks { 79 | fun onListItemClick(demo: Demo) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/kyrie/HeartbreakFragment.kt: -------------------------------------------------------------------------------- 1 | package com.example.kyrie 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.SeekBar 9 | import androidx.fragment.app.Fragment 10 | 11 | import com.github.alexjlockwood.kyrie.KyrieDrawable 12 | 13 | class HeartbreakFragment : Fragment() { 14 | 15 | private lateinit var imageView: ImageView 16 | private lateinit var seekBar: SeekBar 17 | 18 | override fun onCreateView( 19 | inflater: LayoutInflater, 20 | container: ViewGroup?, 21 | savedInstanceState: Bundle?): View? { 22 | val view = inflater.inflate(R.layout.fragment_seekbar, container, false) 23 | imageView = view.findViewById(R.id.image_view) 24 | seekBar = view.findViewById(R.id.seekbar) 25 | return view 26 | } 27 | 28 | override fun onActivityCreated(savedInstanceState: Bundle?) { 29 | super.onActivityCreated(savedInstanceState) 30 | 31 | val drawable = KyrieDrawable.create(requireContext(), R.drawable.avd_heartbreak)!! 32 | drawable.addListener(SampleListenerAdapter(seekBar)) 33 | imageView.setImageDrawable(drawable) 34 | imageView.setOnClickListener(SampleOnClickListener(drawable)) 35 | seekBar.setOnSeekBarChangeListener(SampleOnSeekBarChangeListener(drawable)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/kyrie/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.kyrie 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.fragment.app.FragmentManager 6 | 7 | private const val STATE_TITLE = "state_title" 8 | 9 | class MainActivity : AppCompatActivity(), DemoListFragment.Callbacks, FragmentManager.OnBackStackChangedListener { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_main) 14 | 15 | supportFragmentManager.addOnBackStackChangedListener(this) 16 | if (savedInstanceState == null) { 17 | supportFragmentManager 18 | .beginTransaction() 19 | .add(R.id.container, DemoListFragment()) 20 | .commit() 21 | } else { 22 | supportActionBar!!.setTitle(savedInstanceState.getString(STATE_TITLE)) 23 | } 24 | } 25 | 26 | override fun onSaveInstanceState(outState: Bundle) { 27 | super.onSaveInstanceState(outState) 28 | outState.putCharSequence(STATE_TITLE, supportActionBar!!.title) 29 | } 30 | 31 | override fun onListItemClick(demo: DemoListFragment.Demo) { 32 | val fragmentName = demo.fragmentClassName 33 | val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, fragmentName) 34 | supportFragmentManager 35 | .beginTransaction() 36 | .replace(R.id.container, fragment) 37 | .addToBackStack(demo.title) 38 | .commit() 39 | } 40 | 41 | 42 | override fun onBackStackChanged() { 43 | // This is pretty hacky but whatevs... 44 | val entryCount = supportFragmentManager.backStackEntryCount 45 | val title = if (entryCount == 0) { 46 | getString(R.string.app_name) 47 | } else { 48 | supportFragmentManager.getBackStackEntryAt(entryCount - 1).name 49 | } 50 | setTitle(title) 51 | } 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/kyrie/PathMorphFragment.java: -------------------------------------------------------------------------------- 1 | package com.example.kyrie; 2 | 3 | import android.content.Context; 4 | import android.graphics.Color; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.SeekBar; 11 | 12 | import androidx.annotation.NonNull; 13 | import androidx.annotation.Nullable; 14 | import androidx.core.content.ContextCompat; 15 | import androidx.fragment.app.Fragment; 16 | 17 | import com.github.alexjlockwood.kyrie.Animation; 18 | import com.github.alexjlockwood.kyrie.Keyframe; 19 | import com.github.alexjlockwood.kyrie.KyrieDrawable; 20 | import com.github.alexjlockwood.kyrie.PathData; 21 | import com.github.alexjlockwood.kyrie.PathNode; 22 | 23 | public class PathMorphFragment extends Fragment { 24 | private ImageView imageView; 25 | private SeekBar seekBar; 26 | 27 | @Nullable 28 | @Override 29 | public View onCreateView( 30 | @NonNull LayoutInflater inflater, 31 | @Nullable ViewGroup container, 32 | @Nullable Bundle savedInstanceState) { 33 | final View view = inflater.inflate(R.layout.fragment_seekbar, container, false); 34 | imageView = view.findViewById(R.id.image_view); 35 | seekBar = view.findViewById(R.id.seekbar); 36 | return view; 37 | } 38 | 39 | @Override 40 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 41 | super.onActivityCreated(savedInstanceState); 42 | 43 | final KyrieDrawable drawable = createDrawable(); 44 | imageView.setImageDrawable(drawable); 45 | imageView.setOnClickListener(new SampleOnClickListener(drawable)); 46 | seekBar.setOnSeekBarChangeListener(new SampleOnSeekBarChangeListener(drawable)); 47 | } 48 | 49 | private KyrieDrawable createDrawable() { 50 | final Context ctx = requireContext(); 51 | final PathData hippoPathData = PathData.parse(getString(R.string.hippo)); 52 | final PathData elephantPathData = PathData.parse(getString(R.string.elephant)); 53 | final PathData buffaloPathData = PathData.parse(getString(R.string.buffalo)); 54 | final int hippoFillColor = ContextCompat.getColor(ctx, R.color.hippo); 55 | final int elephantFillColor = ContextCompat.getColor(ctx, R.color.elephant); 56 | final int buffaloFillColor = ContextCompat.getColor(ctx, R.color.buffalo); 57 | final KyrieDrawable kyrieDrawable = 58 | KyrieDrawable.builder() 59 | .viewport(409, 280) 60 | .child( 61 | PathNode.builder() 62 | .strokeColor(Color.BLACK) 63 | .strokeWidth(1f) 64 | .fillColor( 65 | Animation.ofArgb(hippoFillColor, elephantFillColor).duration(300), 66 | Animation.ofArgb(buffaloFillColor).startDelay(600).duration(300), 67 | Animation.ofArgb(hippoFillColor).startDelay(1200).duration(300)) 68 | .pathData( 69 | Animation.ofPathMorph( 70 | Keyframe.of(0, hippoPathData), 71 | Keyframe.of(0.2f, elephantPathData), 72 | Keyframe.of(0.4f, elephantPathData), 73 | Keyframe.of(0.6f, buffaloPathData), 74 | Keyframe.of(0.8f, buffaloPathData), 75 | Keyframe.of(1, hippoPathData)) 76 | .duration(1500))) 77 | .build(); 78 | kyrieDrawable.addListener(new SampleListenerAdapter(seekBar)); 79 | return kyrieDrawable; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/kyrie/PolygonsFragment.java: -------------------------------------------------------------------------------- 1 | package com.example.kyrie; 2 | 3 | import android.graphics.Color; 4 | import android.graphics.PointF; 5 | import android.os.Bundle; 6 | import android.text.TextUtils; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ImageView; 11 | 12 | import androidx.annotation.ColorInt; 13 | import androidx.annotation.NonNull; 14 | import androidx.annotation.Nullable; 15 | import androidx.fragment.app.Fragment; 16 | 17 | import com.github.alexjlockwood.kyrie.Animation; 18 | import com.github.alexjlockwood.kyrie.CircleNode; 19 | import com.github.alexjlockwood.kyrie.KyrieDrawable; 20 | import com.github.alexjlockwood.kyrie.PathData; 21 | import com.github.alexjlockwood.kyrie.PathNode; 22 | 23 | import java.util.ArrayList; 24 | import java.util.Collections; 25 | import java.util.List; 26 | 27 | public class PolygonsFragment extends Fragment { 28 | private static final float VIEWPORT_WIDTH = 1080; 29 | private static final float VIEWPORT_HEIGHT = 1080; 30 | private static final int DURATION = 7500; 31 | 32 | private final Polygon[] polygons = { 33 | new Polygon(15, 0xffe84c65, 362f, 2), 34 | new Polygon(14, 0xffe84c65, 338f, 3), 35 | new Polygon(13, 0xffd554d9, 314f, 4), 36 | new Polygon(12, 0xffaf6eee, 292f, 5), 37 | new Polygon(11, 0xff4a4ae6, 268f, 6), 38 | new Polygon(10, 0xff4294e7, 244f, 7), 39 | new Polygon(9, 0xff6beeee, 220f, 8), 40 | new Polygon(8, 0xff42e794, 196f, 9), 41 | new Polygon(7, 0xff5ae75a, 172f, 10), 42 | new Polygon(6, 0xffade76b, 148f, 11), 43 | new Polygon(5, 0xffefefbb, 128f, 12), 44 | new Polygon(4, 0xffe79442, 106f, 13), 45 | new Polygon(3, 0xffe84c65, 90f, 14) 46 | }; 47 | 48 | private View rootView; 49 | private ImageView imageViewLaps; 50 | private ImageView imageViewVortex; 51 | 52 | @Nullable 53 | @Override 54 | public View onCreateView( 55 | @NonNull LayoutInflater inflater, 56 | @Nullable ViewGroup container, 57 | @Nullable Bundle savedInstanceState) { 58 | rootView = inflater.inflate(R.layout.fragment_two_pane, container, false); 59 | imageViewLaps = rootView.findViewById(R.id.imageViewPane1); 60 | imageViewVortex = rootView.findViewById(R.id.imageViewPane2); 61 | return rootView; 62 | } 63 | 64 | @Override 65 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 66 | super.onActivityCreated(savedInstanceState); 67 | 68 | final KyrieDrawable lapsDrawable = createLapsDrawable(); 69 | imageViewLaps.setImageDrawable(lapsDrawable); 70 | 71 | final KyrieDrawable vortexDrawable = createVortexDrawable(); 72 | imageViewVortex.setImageDrawable(vortexDrawable); 73 | 74 | rootView.setOnClickListener( 75 | v -> { 76 | lapsDrawable.start(); 77 | vortexDrawable.start(); 78 | }); 79 | } 80 | 81 | private KyrieDrawable createLapsDrawable() { 82 | final KyrieDrawable.Builder builder = 83 | KyrieDrawable.builder().viewport(VIEWPORT_WIDTH, VIEWPORT_HEIGHT); 84 | 85 | for (Polygon polygon : polygons) { 86 | builder.child( 87 | PathNode.builder() 88 | .pathData(PathData.parse(polygon.pathData)) 89 | .strokeWidth(4f) 90 | .strokeColor(polygon.color)); 91 | } 92 | 93 | for (Polygon polygon : polygons) { 94 | final PathData pathData = 95 | PathData.parse(TextUtils.join(" ", Collections.nCopies(polygon.laps, polygon.pathData))); 96 | final Animation pathMotion = 97 | Animation.ofPathMotion(PathData.toPath(pathData)) 98 | .repeatCount(Animation.INFINITE) 99 | .duration(DURATION); 100 | builder.child( 101 | CircleNode.builder() 102 | .fillColor(Color.BLACK) 103 | .radius(8) 104 | .centerX(pathMotion.transform(p -> p.x)) 105 | .centerY(pathMotion.transform(p -> p.y))); 106 | } 107 | 108 | return builder.build(); 109 | } 110 | 111 | private KyrieDrawable createVortexDrawable() { 112 | final KyrieDrawable.Builder builder = 113 | KyrieDrawable.builder().viewport(VIEWPORT_WIDTH, VIEWPORT_HEIGHT); 114 | 115 | for (Polygon polygon : polygons) { 116 | final float length = polygon.length; 117 | final float totalLength = length * polygon.laps; 118 | builder.child( 119 | PathNode.builder() 120 | .pathData(PathData.parse(polygon.pathData)) 121 | .strokeWidth(4f) 122 | .strokeColor(polygon.color) 123 | .strokeDashArray( 124 | Animation.ofFloatArray(new float[] {0, length}, new float[] {length, 0}) 125 | .repeatCount(Animation.INFINITE) 126 | .duration(DURATION)) 127 | .strokeDashOffset( 128 | Animation.ofFloat(0f, 2 * totalLength) 129 | .repeatCount(Animation.INFINITE) 130 | .duration(DURATION))); 131 | } 132 | 133 | return builder.build(); 134 | } 135 | 136 | private static class Polygon { 137 | final int sides; 138 | @ColorInt final int color; 139 | final float radius; 140 | final int laps; 141 | final String pathData; 142 | final float length; 143 | 144 | Polygon(int sides, @ColorInt int color, float radius, int laps) { 145 | this.sides = sides; 146 | this.color = color; 147 | this.radius = radius; 148 | this.laps = laps; 149 | final List points = getPoints(sides, radius); 150 | this.pathData = pointsToPathData(points); 151 | this.length = pointsToLength(points); 152 | } 153 | 154 | private static List getPoints(int sides, float radius) { 155 | final List points = new ArrayList<>(sides); 156 | final float angle = (float) (2 * Math.PI / sides); 157 | final float startAngle = (float) (3 * Math.PI / 2); 158 | for (int i = 0; i <= sides; i++) { 159 | final float theta = startAngle + angle * i; 160 | points.add(getPolygonPoint(radius, theta)); 161 | } 162 | return points; 163 | } 164 | 165 | private static PointF getPolygonPoint(float radius, float theta) { 166 | return new PointF( 167 | (VIEWPORT_WIDTH / 2) + (float) (radius * Math.cos(theta)), 168 | (VIEWPORT_HEIGHT / 2) + (float) (radius * Math.sin(theta))); 169 | } 170 | 171 | private static String pointsToPathData(List points) { 172 | final StringBuilder sb = new StringBuilder("M"); 173 | for (PointF p : points) { 174 | sb.append(" ").append(p.x).append(" ").append(p.y); 175 | } 176 | return sb.toString(); 177 | } 178 | 179 | private static float pointsToLength(List points) { 180 | float length = 0; 181 | for (int i = 1, size = points.size(); i < size; i++) { 182 | final PointF prev = points.get(i - 1); 183 | final PointF curr = points.get(i); 184 | length += Math.hypot(curr.x - prev.x, curr.y - prev.y); 185 | } 186 | return length; 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/example/kyrie/ProgressFragment.kt: -------------------------------------------------------------------------------- 1 | package com.example.kyrie 2 | 3 | import android.graphics.Color 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.core.content.ContextCompat 9 | import androidx.fragment.app.Fragment 10 | import com.github.alexjlockwood.kyrie.Animation 11 | import com.github.alexjlockwood.kyrie.KyrieDrawable 12 | import com.github.alexjlockwood.kyrie.StrokeLineCap 13 | import com.github.alexjlockwood.kyrie.asPath 14 | import com.github.alexjlockwood.kyrie.asPathInterpolator 15 | import com.github.alexjlockwood.kyrie.group 16 | import com.github.alexjlockwood.kyrie.kyrieDrawable 17 | import com.github.alexjlockwood.kyrie.path 18 | import kotlinx.android.synthetic.main.fragment_two_pane.* 19 | 20 | class ProgressFragment : Fragment() { 21 | 22 | override fun onCreateView( 23 | inflater: LayoutInflater, 24 | container: ViewGroup?, 25 | savedInstanceState: Bundle? 26 | ): View? = inflater.inflate(R.layout.fragment_two_pane, container, false) 27 | 28 | override fun onActivityCreated(savedInstanceState: Bundle?) { 29 | super.onActivityCreated(savedInstanceState) 30 | 31 | val horizontalDrawable = createHorizontalDrawable() 32 | imageViewPane1.setImageDrawable(horizontalDrawable) 33 | horizontalDrawable.start() 34 | 35 | val circularDrawable = createCircularDrawable() 36 | imageViewPane2.setImageDrawable(circularDrawable) 37 | circularDrawable.start() 38 | } 39 | 40 | private fun createHorizontalDrawable(): KyrieDrawable { 41 | val tintColor = ContextCompat.getColor(requireContext(), R.color.colorAccent) 42 | return kyrieDrawable { 43 | viewport(360f, 10f) 44 | tint(tintColor) 45 | group { 46 | translateX(180f) 47 | translateY(5f) 48 | path { 49 | fillAlpha(0.3f) 50 | fillColor(Color.WHITE) 51 | pathData("M -180,-1 l 360,0 l 0,2 l -360,0 Z") 52 | } 53 | path { 54 | scaleX( 55 | Animation.ofPathMotion("M 0 0.1 L 1 0.571 L 2 0.91 L 3 0.1".asPath()) 56 | .transform { p -> p.y } 57 | .duration(2000) 58 | .repeatCount(Animation.INFINITE) 59 | .interpolator("M 0 0 C 0.068 0.02 0.192 0.159 0.333 0.349 C 0.384 0.415 0.549 0.681 0.667 0.683 C 0.753 0.682 0.737 0.879 1 1".asPathInterpolator())) 60 | translateX( 61 | Animation.ofPathMotion("M -197.6 0 C -183.318 0 -112.522 0 -62.053 0 C -7.791 0 28.371 0 106.19 0 C 250.912 0 422.6 0 422.6 0".asPath()) 62 | .transform { p -> p.x } 63 | .duration(2000) 64 | .repeatCount(Animation.INFINITE) 65 | .interpolator("M 0 0 C 0.037 0 0.129 0.09 0.25 0.219 C 0.322 0.296 0.437 0.418 0.483 0.49 C 0.69 0.81 0.793 0.95 1 1".asPathInterpolator())) 66 | 67 | fillColor(Color.WHITE) 68 | pathData("M -144,-1 l 288,0 l 0,2 l -288,0 Z") 69 | } 70 | path { 71 | scaleX( 72 | Animation.ofPathMotion("M 0 0.1 L 1 0.826 L 2 0.1".asPath()) 73 | .transform { p -> p.y } 74 | .duration(2000) 75 | .repeatCount(Animation.INFINITE) 76 | .interpolator("M 0 0 L 0.366 0 C 0.473 0.062 0.615 0.5 0.683 0.5 C 0.755 0.5 0.757 0.815 1 1".asPathInterpolator())) 77 | translateX( 78 | Animation.ofPathMotion("M -522.6 0 C -473.7 0 -356.573 0 -221.383 0 C -23.801 0 199.6 0 199.6 0".asPath()) 79 | .transform { p -> p.x } 80 | .duration(2000) 81 | .repeatCount(Animation.INFINITE) 82 | .interpolator("M 0 0 L 0.2 0 C 0.395 0 0.474 0.206 0.591 0.417 C 0.715 0.639 0.816 0.974 1 1".asPathInterpolator())) 83 | fillColor(Color.WHITE) 84 | pathData("M -144,-1 l 288,0 l 0,2 l -288,0 Z") 85 | } 86 | } 87 | } 88 | } 89 | 90 | private fun createCircularDrawable(): KyrieDrawable { 91 | val tintColor = ContextCompat.getColor(requireContext(), R.color.colorAccent) 92 | return kyrieDrawable { 93 | viewport(48f, 48f) 94 | tint(tintColor) 95 | group { 96 | translateX(24f) 97 | translateY(24f) 98 | rotation( 99 | Animation.ofFloat(0f, 720f) 100 | .duration(4444) 101 | .repeatCount(Animation.INFINITE) 102 | ) 103 | path { 104 | strokeColor(Color.WHITE) 105 | strokeWidth(4f) 106 | trimPathStart( 107 | Animation.ofFloat(0f, 0.75f) 108 | .duration(1333) 109 | .repeatCount(Animation.INFINITE) 110 | .interpolator("M 0 0 L 0.5 0 C 0.7 0 0.6 1 1 1".asPathInterpolator()) 111 | ) 112 | trimPathEnd( 113 | Animation.ofFloat(0.03f, 0.78f) 114 | .duration(1333) 115 | .repeatCount(Animation.INFINITE) 116 | .interpolator("M 0 0 C 0.2 0 0.1 1 0.5 0.96 C 0.966 0.96 0.993 1 1 1".asPathInterpolator()) 117 | ) 118 | trimPathOffset( 119 | Animation.ofFloat(0f, 0.25f) 120 | .duration(1333) 121 | .repeatCount(Animation.INFINITE) 122 | ) 123 | strokeLineCap(StrokeLineCap.SQUARE) 124 | pathData("M 0 0 m 0 -18 a 18 18 0 1 1 0 36 a 18 18 0 1 1 0 -36") 125 | } 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/kyrie/SampleListenerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.example.kyrie 2 | 3 | import android.widget.SeekBar 4 | 5 | import com.github.alexjlockwood.kyrie.KyrieDrawable 6 | 7 | internal class SampleListenerAdapter(private val seekBar: SeekBar) : KyrieDrawable.ListenerAdapter() { 8 | 9 | override fun onAnimationUpdate(drawable: KyrieDrawable) { 10 | val playTime = drawable.currentPlayTime.toFloat() 11 | val totalDuration = drawable.totalDuration.toFloat() 12 | val fraction = playTime / totalDuration 13 | seekBar.progress = Math.round(fraction * seekBar.max) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/kyrie/SampleOnClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.example.kyrie 2 | 3 | import android.view.View 4 | 5 | import com.github.alexjlockwood.kyrie.KyrieDrawable 6 | 7 | internal class SampleOnClickListener(private val drawable: KyrieDrawable) : View.OnClickListener { 8 | 9 | override fun onClick(v: View) { 10 | if (drawable.isPaused) { 11 | drawable.resume() 12 | } else { 13 | if (drawable.isStarted) { 14 | drawable.pause() 15 | } else { 16 | drawable.start() 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/kyrie/SampleOnSeekBarChangeListener.kt: -------------------------------------------------------------------------------- 1 | package com.example.kyrie 2 | 3 | import android.widget.SeekBar 4 | 5 | import com.github.alexjlockwood.kyrie.KyrieDrawable 6 | 7 | internal class SampleOnSeekBarChangeListener(private val drawable: KyrieDrawable) : SeekBar.OnSeekBarChangeListener { 8 | 9 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 10 | val totalDuration = drawable.totalDuration 11 | drawable.currentPlayTime = (progress / 100f * totalDuration).toLong() 12 | } 13 | 14 | override fun onStartTrackingTouch(seekBar: SeekBar) { 15 | if (drawable.isRunning) { 16 | drawable.pause() 17 | } 18 | } 19 | 20 | override fun onStopTrackingTouch(seekBar: SeekBar) {} 21 | } 22 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/avd_heartbreak.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 17 | 21 | 22 | 26 | 30 | 31 | 32 | 38 | 39 | 40 | 46 | 47 | 48 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 166 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/src/main/res/layout-land/fragment_two_pane.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/fragment_demo_item.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/fragment_demo_list.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/fragment_seekbar.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/fragment_two_pane.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjlockwood/kyrie/539d05b456782e0186d28aca29a86e9cd0132875/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #007a33 4 | #004d08 5 | #007a33 6 | 7 | @color/colorPrimary 8 | 9 | -------------------------------------------------------------------------------- /sample/src/main/res/values/heartbreak.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | M 28.031 21.054 C 28.02 21.066 28.01 21.078 28 21.09 C 26.91 19.81 25.24 19 23.5 19 C 20.42 19 18 21.42 18 24.5 C 18 28.28 21.4 31.36 26.55 36.03 L 28 37.35 L 28.002 37.348 L 27.781 36.988 L 28.489 36.073 L 27.506 34.764 L 28.782 33.027 L 26.944 31.008 L 29.149 28.725 L 27.117 27.143 L 29.149 25.018 L 26.488 22.977 L 28.031 21.054 L 28.031 21.054 Z 4 | M 28.031 21.054 C 28.169 20.895 28.316 20.743 28.471 20.599 L 28.915 20.226 C 29.926 19.457 31.193 19 32.5 19 C 35.58 19 38 21.42 38 24.5 C 38 28.28 34.6 31.36 29.45 36.04 L 28.002 37.348 L 27.781 36.988 L 28.489 36.073 L 27.506 34.764 L 28.782 33.027 L 26.944 31.008 L 29.149 28.725 L 27.117 27.143 L 29.149 25.018 L 26.488 22.977 L 28.031 21.054 L 28.031 21.054 Z 5 | M 28.719 38.296 L 25.669 35.552 C 21.621 31.793 18.016 28.891 18.016 24.845 C 18.016 21.588 20.631 19.965 23.634 19.965 C 24.999 19.965 26.799 21.181 28.644 23.13 6 | M 27.231 38.294 L 30.765 35.2 C 34.834 31.235 37.752 29.118 38.004 25.084 C 38.168 22.459 35.773 20.035 33.379 20.035 C 30.432 20.035 29.672 21.047 27.231 23.133 7 | M 18 38 C 18 38 24 38 24 38 C 24 38 32 38 32 38 C 32 38 38 38 38 38 L 38 38 L 18 38 L 18 38 Z 8 | M 18 26 C 18 26 21 28 24 28 C 27 28 29 25 32 25 C 35 25 38 26 38 26 L 38 38 L 18 38 L 18 26 Z 9 | M 18 18 C 18 18 24 18 24 18 C 24 18 32 18 32 18 C 32 18 38 18 38 18 L 38 38 L 18 38 L 18 18 Z 10 | M 28 39 L 26.405 37.567 C 20.74 32.471 17 29.109 17 24.995 C 17 21.632 19.657 19 23.05 19 C 24.964 19 26.801 19.883 28 21.272 C 29.199 19.883 31.036 19 32.95 19 C 36.343 19 39 21.632 39 24.995 C 39 29.109 35.26 32.471 29.595 37.567 L 28 39 L 28 39 Z 11 | 12 | 13 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Kyrie 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Instructions 2 | 3 | Increment the version numbers in `kyrie/build.gradle` and `README.md`. 4 | 5 | Then run the following: 6 | 7 | ```sh 8 | ./gradlew clean build bintrayUpload -PbintrayUser=alexjlockwood -PbintrayKey=BINTRAY_API_KEY -PdryRun=false 9 | ``` 10 | -------------------------------------------------------------------------------- /scripts/publish.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.novoda.bintray-release' 2 | 3 | publish { 4 | def groupProjectID = publishedGroupId 5 | def artifactProjectID = artifact 6 | def publishVersionID = libraryVersion 7 | 8 | userOrg = developerId 9 | repoName = bintrayRepo 10 | groupId = groupProjectID 11 | artifactId = artifactProjectID 12 | publishVersion = publishVersionID 13 | desc = libraryDescription 14 | website = siteUrl 15 | } 16 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':kyrie' 2 | include ':sample' 3 | --------------------------------------------------------------------------------