├── settings.gradle
├── art
├── screencap.gif
└── hippo-elephant.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── sample
├── src
│ └── main
│ │ ├── ic_launcher-web.png
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ ├── styles.xml
│ │ │ └── heartbreak.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
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── layout
│ │ │ ├── fragment_demo_list.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── fragment_demo_item.xml
│ │ │ ├── fragment_seekbar.xml
│ │ │ └── fragment_two_pane.xml
│ │ ├── layout-land
│ │ │ └── fragment_two_pane.xml
│ │ └── drawable
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ └── avd_heartbreak.xml
│ │ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── kyrie
│ │ │ ├── SampleOnClickListener.kt
│ │ │ ├── SampleListenerAdapter.kt
│ │ │ ├── SampleOnSeekBarChangeListener.kt
│ │ │ ├── HeartbreakFragment.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── DemoListFragment.kt
│ │ │ ├── PathMorphFragment.java
│ │ │ ├── ProgressFragment.kt
│ │ │ └── PolygonsFragment.java
│ │ └── AndroidManifest.xml
└── build.gradle
├── kyrie
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── github
│ │ └── alexjlockwood
│ │ └── kyrie
│ │ ├── FillType.kt
│ │ ├── StrokeLineCap.kt
│ │ ├── ClipType.kt
│ │ ├── StrokeLineJoin.kt
│ │ ├── PropertyTimeline.kt
│ │ ├── package-info.java
│ │ ├── Extensions.kt
│ │ ├── ObjectKeyframeSet.kt
│ │ ├── KeyframeSet.kt
│ │ ├── GroupNode.kt
│ │ ├── Node.kt
│ │ ├── PathData.kt
│ │ ├── PathNode.kt
│ │ ├── ComplexColor.java
│ │ ├── ClipPathNode.kt
│ │ ├── Keyframe.kt
│ │ ├── Styleable.java
│ │ ├── CircleNode.kt
│ │ ├── PathKeyframeSet.kt
│ │ ├── LineNode.kt
│ │ ├── EllipseNode.kt
│ │ ├── Property.kt
│ │ ├── TransformNode.kt
│ │ ├── GradientColorInflater.java
│ │ ├── RectangleNode.kt
│ │ └── TypedArrayUtils.kt
└── build.gradle
├── scripts
├── README.md
└── publish.gradle
├── .gitignore
├── .travis.yml
├── gradle.properties
├── gradlew.bat
├── gradlew
├── README.md
└── LICENSE
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':kyrie'
2 | include ':sample'
3 |
--------------------------------------------------------------------------------
/art/screencap.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/art/screencap.gif
--------------------------------------------------------------------------------
/art/hippo-elephant.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/art/hippo-elephant.gif
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/sample/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Kyrie
4 |
5 |
6 |
--------------------------------------------------------------------------------
/kyrie/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexjlockwood/kyrie/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/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/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/fragment_demo_list.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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/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/res/layout/fragment_demo_item.xml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/layout-land/fragment_two_pane.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
19 |
20 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
8 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 | * - A Gradient; as represented by a {@link Shader}.
30 | *
- A {@link ColorStateList}
31 | *
- 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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kyrie
2 |
3 | [![Build status][travis-badge]][travis-badge-url]
4 | [](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 | 
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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------