├── .gitignore
├── .idea
├── assetWizardSettings.xml
├── codeStyles
│ └── Project.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── .project
├── .settings
└── org.eclipse.buildship.core.prefs
├── LICENSE
├── README.md
├── art
├── banner.png
├── play_store_badge.png
└── preview.gif
├── build.gradle
├── demo
├── .classpath
├── .gitignore
├── .project
├── .settings
│ └── org.eclipse.buildship.core.prefs
├── build.gradle
├── proguard-rules.pro
├── release
│ └── output.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── github
│ │ └── sumimakito
│ │ └── rhythmviewdemo
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-web.png
│ ├── java
│ │ └── com
│ │ │ └── github
│ │ │ └── sumimakito
│ │ │ ├── cappuccino
│ │ │ └── ContentHelper.kt
│ │ │ └── rhythmviewdemo
│ │ │ ├── MainActivity.kt
│ │ │ └── PromptActivity.kt
│ └── res
│ │ ├── drawable
│ │ ├── background.xml
│ │ ├── ic_check_white_24dp.xml
│ │ ├── ic_help_white_24dp.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_library_music_white_24dp.xml
│ │ ├── ic_photo_library_white_24dp.xml
│ │ └── ic_settings_white_24dp.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── activity_prompt.xml
│ │ └── view_help.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── raw
│ │ └── cover.jpg
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── file_provider_paths.xml
│ └── test
│ └── java
│ └── com
│ └── github
│ └── sumimakito
│ └── rhythmviewdemo
│ └── ExampleUnitTest.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .classpath
├── .gitignore
├── .project
├── .settings
│ └── org.eclipse.buildship.core.prefs
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── github
│ │ └── sumimakito
│ │ └── rhythmview
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── github
│ │ └── sumimakito
│ │ └── rhythmview
│ │ ├── RhythmView.kt
│ │ ├── datasource
│ │ ├── BaseDataSource.kt
│ │ └── PlaybackSource.kt
│ │ ├── effect
│ │ ├── BaseEffect.kt
│ │ ├── RainbowRay.kt
│ │ ├── Ray.kt
│ │ └── Ripple.kt
│ │ ├── particle
│ │ ├── Particle.kt
│ │ └── ParticleManager.kt
│ │ ├── util
│ │ └── MathUtils.kt
│ │ └── wave
│ │ └── WavePoint.kt
│ └── test
│ └── java
│ └── com
│ └── github
│ └── sumimakito
│ └── rhythmview
│ └── ExampleUnitTest.kt
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches/build_file_checksums.ser
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | .DS_Store
9 | /build
10 | /captures
11 | .externalNativeBuild
12 | *.apk
13 | *.zip
14 |
15 |
16 | # Created by https://www.gitignore.io/api/intellij
17 |
18 | ### Intellij ###
19 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
20 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
21 |
22 | # User-specific stuff
23 | .idea/**/workspace.xml
24 | .idea/**/tasks.xml
25 | .idea/**/usage.statistics.xml
26 | .idea/**/dictionaries
27 | .idea/**/shelf
28 |
29 | # Generated files
30 | .idea/**/contentModel.xml
31 |
32 | # Sensitive or high-churn files
33 | .idea/**/dataSources/
34 | .idea/**/dataSources.ids
35 | .idea/**/dataSources.local.xml
36 | .idea/**/sqlDataSources.xml
37 | .idea/**/dynamic.xml
38 | .idea/**/uiDesigner.xml
39 | .idea/**/dbnavigator.xml
40 |
41 | # Gradle
42 | .idea/**/gradle.xml
43 | .idea/**/libraries
44 |
45 | # Gradle and Maven with auto-import
46 | # When using Gradle or Maven with auto-import, you should exclude module files,
47 | # since they will be recreated, and may cause churn. Uncomment if using
48 | # auto-import.
49 | # .idea/modules.xml
50 | # .idea/*.iml
51 | # .idea/modules
52 |
53 | # CMake
54 | cmake-build-*/
55 |
56 | # Mongo Explorer plugin
57 | .idea/**/mongoSettings.xml
58 |
59 | # File-based project format
60 | *.iws
61 |
62 | # IntelliJ
63 | out/
64 |
65 | # mpeltonen/sbt-idea plugin
66 | .idea_modules/
67 |
68 | # JIRA plugin
69 | atlassian-ide-plugin.xml
70 |
71 | # Cursive Clojure plugin
72 | .idea/replstate.xml
73 |
74 | # Crashlytics plugin (for Android Studio and IntelliJ)
75 | com_crashlytics_export_strings.xml
76 | crashlytics.properties
77 | crashlytics-build.properties
78 | fabric.properties
79 |
80 | # Editor-based Rest Client
81 | .idea/httpRequests
82 |
83 | ### Intellij Patch ###
84 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
85 |
86 | # *.iml
87 | # modules.xml
88 | # .idea/misc.xml
89 | # *.ipr
90 |
91 | # Sonarlint plugin
92 | .idea/sonarlint
93 |
94 |
95 | # End of https://www.gitignore.io/api/intellij
96 |
--------------------------------------------------------------------------------
/.idea/assetWizardSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
109 |
110 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
18 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | RhythmView
4 | Project RhythmView created by Buildship.
5 |
6 |
7 |
8 |
9 | org.eclipse.buildship.core.gradleprojectbuilder
10 |
11 |
12 |
13 |
14 |
15 | org.eclipse.buildship.core.gradleprojectnature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.settings/org.eclipse.buildship.core.prefs:
--------------------------------------------------------------------------------
1 | connection.project.dir=
2 | eclipse.preferences.version=1
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 | [](https://jitpack.io/#SumiMakito/RhythmView)
6 | [](https://github.com/SumiMakito/RhythmView/releases/latest)
7 | [](https://github.com/SumiMakito/RhythmView/blob/master/LICENSE)
8 |
9 | RhythmView - Visualizes rhythms for you.
10 |
11 | ### Demo app
12 |
13 | > Try all preset visual effects and play with the options like a master!
14 |
15 |
16 |
17 | If you have difficulties accessing the Play Store, you may find APK files in [releases](https://github.com/SumiMakito/RhythmView/releases).
18 |
19 | ### Preview
20 |
21 | 
22 |
23 |
24 |
25 | ### Quick start
26 |
27 | #### 1. Add the dependency
28 |
29 | Firstly, add lines below in the project-level build.gradle:
30 |
31 | ```
32 | allprojects {
33 | repositories {
34 | ...
35 | maven { url 'https://jitpack.io' }
36 | }
37 | }
38 | ```
39 |
40 | Then, add lines below in the module-level build.gradle:
41 |
42 | > Replace `` with the latest version name.
43 |
44 | ```
45 | dependencies {
46 | compile 'com.github.SumiMakito:RhythmView:'
47 | }
48 | ```
49 |
50 | #### 2. Add the view to a layout
51 |
52 | > RhythmView does not have custom attributes in the XML layout.
53 |
54 | ```xml
55 |
60 | ```
61 |
62 | #### 3. Access in Java/Kotlin code
63 |
64 | ```kotlin
65 | // Kotlin
66 | rhythmView.onRhythmViewLayoutChangedListener = { view: RhythmView ->
67 | // ... setup RhythmView
68 | }
69 | ```
70 |
71 | ```java
72 | // Java
73 | rhythmView.setOnRhythmViewLayoutChangedListener(view -> {
74 | // ... setup RhythmView
75 | });
76 | ```
77 |
78 | ### Example: Ripple & PlaybackSource
79 |
80 | ```java
81 | // Java
82 | ...
83 | public class PlayerActivity extends Activity {
84 | private MediaPlayer mediaPlayer; // to avoid the MediaPlayer being collected by GC
85 | private boolean mediaPlayerInitialized = false;
86 | ...
87 | @Override
88 | protected void onCreate(Bundle savedInstanceState) {
89 | super.onCreate(savedInstanceState);
90 | ...
91 | int resolution = 8;
92 | ...
93 | // the listener will be triggered every time RhythmView's onLayout() is called,
94 | // data source and visual effect SHOULD be re-initialized in response to layout
95 | // changes
96 | rhythmView.setOnRhythmViewLayoutChangedListener(view -> {
97 | // initialize the MediaPlayer only once
98 | if(!mediaPlayerInitialized){
99 | mediaPlayer = MediaPlayer.create(this, R.raw.music);
100 | mediaPlayer.setLooping(true);
101 | }
102 | // initialize the preset data source
103 | PlaybackSource dataSource = new PlaybackSource(mediaPlayer, resolution);
104 | // initialize the visual effect
105 | Ripple ripple = new Ripple(rhythmView, 8);
106 | // set colors for high, medium, low frequencies and particles
107 | ripple.setColorHF(0xffef9a9a);
108 | ripple.setColorMF(0xffef9a9a);
109 | ripple.setColorLF(0xffef9a9a);
110 | ripple.setColorParticle(0xffef9a9a);
111 | // set the data source for the visual effect
112 | ripple.setDataSource(dataSource);
113 | // start the playback only once
114 | if(!mediaPlayerInitialized){
115 | mediaPlayer.start();
116 | mediaPlayerInitialized = true;
117 | }
118 | });
119 | // set the album cover (this operation can be done outside the listener)
120 | rhythmView.setAlbumCover(coverBitmap);
121 | ...
122 | }
123 | ...
124 | @Override
125 | protected void onDestroy() {
126 | super.onDestroy();
127 | // remember to release the MediaPlayer instance
128 | mediaPlayer.release();
129 | }
130 | ...
131 | }
132 | ```
133 |
134 | ```kotlin
135 | // Kotlin
136 | ...
137 | class PlayerActivity : Activity() {
138 | private var mediaPlayer: MediaPlayer? = null // to avoid the MediaPlayer being collected by GC
139 | private var mediaPlayerInitialized = false
140 | ...
141 | override fun onCreate(savedInstanceState: Bundle) {
142 | super.onCreate(savedInstanceState)
143 | ...
144 | val resolution = 8
145 | ...
146 | // the listener will be triggered every time RhythmView's onLayout() is called,
147 | // data source and visual effect SHOULD be re-initialized in response to layout
148 | // changes
149 | rhythmView.onRhythmViewLayoutChangedListener = { view: RhythmView ->
150 | // initialize the MediaPlayer only once
151 | if (!mediaPlayerInitialized) {
152 | mediaPlayer = MediaPlayer.create(this, R.raw.music)
153 | mediaPlayer!!.setLooping(true)
154 | }
155 | // initialize the preset data source
156 | val dataSource = PlaybackSource(mediaPlayer!!, resolution)
157 | // initialize the visual effect
158 | val ripple = Ripple(view, 8)
159 | // set colors for high, medium, low frequencies and particles
160 | ripple.colorHF = 0xffef9a9a
161 | ripple.colorMF = 0xffef9a9a
162 | ripple.colorLF = 0xffef9a9a
163 | ripple.colorParticle = 0xffef9a9a
164 | // set the data source for the visual effect
165 | ripple.dataSource = dataSource
166 | // start the playback only once
167 | if (!mediaPlayerInitialized) {
168 | mediaPlayer!!.start()
169 | mediaPlayerInitialized = true
170 | }
171 | }
172 | // set the album cover (this operation can be done outside the listener)
173 | rhythmView.albumCover = coverBitmap
174 | ...
175 | }
176 | ...
177 | override fun onDestroy() {
178 | super.onDestroy()
179 | // remember to release the MediaPlayer instance
180 | mediaPlayer!!.release()
181 | }
182 | ...
183 | }
184 | ```
185 |
186 | ### Metrics & Specs
187 |
188 | ```
189 | innerWidth = width - paddingLeft - paddingRight
190 | innerHeight = height - paddingTop - paddingBottom
191 |
192 | innerSize = min(innerWidth, innerHeight)
193 |
194 | centerX = paddingLeft + innerWidth * 0.5
195 | centerY = paddingTop + innerHeight * 0.5
196 |
197 | maxDrawingWidth = innerSize * maxDrawingWidthScale * 0.5
198 | radius = (innerSize - 2 * maxDrawingWidth - 2 * innerDrawingPaddingScale * innerSize) * 0.5
199 | minDrawingRadius = radius + innerDrawingPaddingScale * innerSize
200 | ```
201 |
202 | ### API definitions
203 |
204 | #### RhythmView
205 |
206 | ###### coverSpinningSpeed
207 |
208 | *Type: Float*
209 |
210 | *Default: 0.5f*
211 |
212 | *Unit: Degrees per frame*
213 |
214 | To control the spinning speed of the cover image.
215 |
216 | Positive value for CW spinning. Negative value for CCW spinning.
217 |
218 | ###### isPaused
219 |
220 | *Type: Boolean*
221 |
222 | *Default: false*
223 |
224 | If true, the cover will stop spinning.
225 |
226 | *To stop the visual effect, please try to stop the MediaPlayer instance bind with the data source.*
227 |
228 | ###### showFpsCounter
229 |
230 | *Type: Boolean*
231 |
232 | *Default: false*
233 |
234 | If true, an FPS counter will display at the top left corner of the view.
235 |
236 | ###### innerDrawingPaddingScale
237 |
238 | *Type: Float*
239 |
240 | *Default: 0.01f*
241 |
242 | ###### maxDrawingWidthScale
243 |
244 | *Type: Float*
245 |
246 | *Default: 0.24f*
247 |
248 | ###### albumCover
249 |
250 | *Type: Bitmap(?Kotlin)*
251 |
252 | *Default: null*
253 |
254 | The cover image for the RhythmView. Will display in a round shape.
255 |
256 | Square images are recommended. Set to null to disable.
257 |
258 | Non-square-shaped images will be resized and fit in to a squared area for clipping.
259 |
260 | ### Donation
261 |
262 | Cheers! Would you like to buy me a cup of Caramel Makito Macchiato?
263 |
264 | You can donate with [PayPal](https://www.paypal.me/makito) or [Alipay](https://qr.alipay.com/a6x02021re1jk4ftcymlw79).
265 |
266 | ### License
267 |
268 |
269 |
270 | RhythmView is available under the Apache-2.0 license.
271 |
272 | See the LICENSE file for more info.
273 |
274 | Copyright © 2017-2018 Makito.
--------------------------------------------------------------------------------
/art/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/art/banner.png
--------------------------------------------------------------------------------
/art/play_store_badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/art/play_store_badge.png
--------------------------------------------------------------------------------
/art/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/art/preview.gif
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.2.50'
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.2.0-beta02'
11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/demo/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/demo/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | demo
4 | Project demo created by Buildship.
5 |
6 |
7 |
8 |
9 | org.eclipse.jdt.core.javabuilder
10 |
11 |
12 |
13 |
14 | org.eclipse.buildship.core.gradleprojectbuilder
15 |
16 |
17 |
18 |
19 |
20 | org.eclipse.jdt.core.javanature
21 | org.eclipse.buildship.core.gradleprojectnature
22 |
23 |
24 |
--------------------------------------------------------------------------------
/demo/.settings/org.eclipse.buildship.core.prefs:
--------------------------------------------------------------------------------
1 | connection.project.dir=..
2 | eclipse.preferences.version=1
3 |
--------------------------------------------------------------------------------
/demo/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | android {
8 | compileSdkVersion 28
9 | defaultConfig {
10 | applicationId "com.github.sumimakito.rhythmview"
11 | minSdkVersion 19
12 | targetSdkVersion 28
13 | versionCode 1
14 | versionName "1.0"
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | compileOptions {
24 | sourceCompatibility JavaVersion.VERSION_1_8
25 | targetCompatibility JavaVersion.VERSION_1_8
26 | }
27 | }
28 |
29 | dependencies {
30 | implementation fileTree(include: ['*.jar'], dir: 'libs')
31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
32 | implementation 'androidx.appcompat:appcompat:1.0.0-alpha1'
33 | implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
34 | testImplementation 'junit:junit:4.12'
35 | androidTestImplementation 'androidx.test:runner:1.1.0-alpha3'
36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha3'
37 | implementation project(':library')
38 | }
39 |
--------------------------------------------------------------------------------
/demo/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/demo/release/output.json:
--------------------------------------------------------------------------------
1 | [{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":1,"versionName":"1.0","enabled":true,"outputFile":"demo-release.apk","fullName":"release","baseName":"release"},"path":"demo-release.apk","properties":{}}]
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/github/sumimakito/rhythmviewdemo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmviewdemo
2 |
3 | import androidx.test.InstrumentationRegistry
4 | import androidx.test.runner.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getTargetContext()
22 | assertEquals("com.github.sumimakito.RhythmView", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/demo/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
30 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/demo/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/demo/src/main/java/com/github/sumimakito/cappuccino/ContentHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.cappuccino
2 |
3 | import android.content.ContentUris
4 | import android.content.Context
5 | import android.database.Cursor
6 | import android.net.Uri
7 | import android.os.Environment
8 | import android.provider.DocumentsContract
9 | import android.provider.MediaStore
10 | import java.io.File
11 |
12 | object ContentHelper {
13 | fun absolutePathFromUri(context: Context, uri: Uri): String? {
14 | if (DocumentsContract.isDocumentUri(context, uri)) {
15 | if (isExternalStorageDocument(uri)) {
16 | val docId = DocumentsContract.getDocumentId(uri)
17 | val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
18 | val type = split[0]
19 |
20 | if ("primary".equals(type, ignoreCase = true)) {
21 | return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
22 | }
23 |
24 | } else if (isDownloadsDocument(uri)) {
25 | val id = DocumentsContract.getDocumentId(uri)
26 | if (RawDocumentsHelper.isRawDocId(id)) {
27 | return RawDocumentsHelper.getAbsoluteFilePath(id)
28 | } else {
29 | val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
30 | return getDataColumn(context, contentUri, null, null)
31 | }
32 | } else if (isMediaDocument(uri)) {
33 | val docId = DocumentsContract.getDocumentId(uri)
34 | val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
35 | val type = split[0]
36 |
37 | var contentUri: Uri? = null
38 | when (type) {
39 | "image" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
40 | "video" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
41 | "audio" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
42 | }
43 |
44 | val selection = "_id=?"
45 | val selectionArgs = arrayOf(split[1])
46 |
47 | return getDataColumn(context, contentUri, selection, selectionArgs)
48 | }
49 | } else if ("content".equals(uri.scheme!!, ignoreCase = true)) {
50 | return getDataColumn(context, uri, null, null)
51 | } else if ("file".equals(uri.scheme!!, ignoreCase = true)) {
52 | return uri.path
53 | }
54 | return null
55 | }
56 |
57 | private fun getDataColumn(context: Context, uri: Uri?, selection: String?,
58 | selectionArgs: Array?): String? {
59 | var cursor: Cursor? = null
60 | val column = "_data"
61 | val projection = arrayOf(column)
62 |
63 | try {
64 | cursor = context.contentResolver.query(uri!!, projection, selection, selectionArgs, null)
65 | if (cursor != null && cursor.moveToFirst()) {
66 | val columnIndex = cursor.getColumnIndexOrThrow(column)
67 | return cursor.getString(columnIndex)
68 | }
69 | } finally {
70 | cursor?.close()
71 | }
72 | return null
73 | }
74 |
75 | private fun isExternalStorageDocument(uri: Uri): Boolean {
76 | return "com.android.externalstorage.documents" == uri.authority
77 | }
78 |
79 | private fun isDownloadsDocument(uri: Uri): Boolean {
80 | return "com.android.providers.downloads.documents" == uri.authority
81 | }
82 |
83 | private fun isMediaDocument(uri: Uri): Boolean {
84 | return "com.android.providers.media.documents" == uri.authority
85 | }
86 |
87 | object RawDocumentsHelper {
88 | private const val RAW_PREFIX = "raw:"
89 |
90 | fun isRawDocId(docId: String?): Boolean {
91 | return docId != null && docId.startsWith(RAW_PREFIX)
92 | }
93 |
94 | fun getDocIdForFile(file: File): String {
95 | return RAW_PREFIX + file.absolutePath
96 | }
97 |
98 | fun getAbsoluteFilePath(rawDocumentId: String): String {
99 | return rawDocumentId.substring(RAW_PREFIX.length)
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/github/sumimakito/rhythmviewdemo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmviewdemo
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.Intent
6 | import android.content.pm.PackageManager
7 | import android.graphics.Bitmap
8 | import android.graphics.BitmapFactory
9 | import android.graphics.Color
10 | import android.media.MediaPlayer
11 | import android.net.Uri
12 | import android.os.Bundle
13 | import android.preference.PreferenceManager
14 | import android.view.View
15 | import android.view.View.GONE
16 | import android.view.View.VISIBLE
17 | import android.widget.SeekBar
18 | import androidx.appcompat.app.AlertDialog
19 | import androidx.appcompat.app.AppCompatActivity
20 | import androidx.core.app.ActivityCompat
21 | import androidx.core.content.ContextCompat
22 | import com.github.sumimakito.cappuccino.ContentHelper
23 | import com.github.sumimakito.rhythmview.datasource.PlaybackSource
24 | import com.github.sumimakito.rhythmview.effect.RainbowRay
25 | import com.github.sumimakito.rhythmview.effect.Ray
26 | import com.github.sumimakito.rhythmview.effect.Ripple
27 | import kotlinx.android.synthetic.main.activity_main.*
28 | import kotlin.math.max
29 | import kotlin.math.min
30 |
31 |
32 | class MainActivity : AppCompatActivity() {
33 | companion object {
34 | const val PERMISSION_OPEN_MUSIC_REQUEST_CODE = 0xCEE
35 | const val PERMISSION_OPEN_IMAGE_REQUEST_CODE = 0xCED
36 | const val OPEN_MUSIC_REQUEST_CODE = 0xCEC
37 | const val OPEN_IMAGE_REQUEST_CODE = 0xCEA
38 | }
39 |
40 | private var dataSource: PlaybackSource? = null
41 | private var mediaPlayer: MediaPlayer? = null
42 | private var divisionValue: Int = 16
43 | private var waveSpeedValue: Float = 0.06f
44 | private var particleSpeedValue: Float = 0.005f
45 | private var colorH: Int = 0xFFFFFFFF.toInt()
46 | private var colorM: Int = 0xFFFFFFFF.toInt()
47 | private var colorL: Int = 0xFFFFFFFF.toInt()
48 |
49 | override fun onCreate(savedInstanceState: Bundle?) {
50 | super.onCreate(savedInstanceState)
51 | setContentView(R.layout.activity_main)
52 |
53 | if (!PreferenceManager.getDefaultSharedPreferences(this).getBoolean("initial_help_shown", false)) {
54 | PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("initial_help_shown", true).apply()
55 | val builder = AlertDialog.Builder(this)
56 | builder.setView(R.layout.view_help)
57 | builder.create().show()
58 | }
59 |
60 | rhythmView.showFpsCounter = true
61 |
62 | rhythmView.onRhythmViewLayoutChangedListener = { view ->
63 | if (view.albumCover == null) {
64 | val cover = BitmapFactory.decodeResource(resources, R.raw.cover)
65 | updateColor(cover)
66 | view.albumCover = cover
67 | }
68 | reloadVisualEffect()
69 | }
70 |
71 | helpButton.setOnClickListener {
72 | val builder = AlertDialog.Builder(this)
73 | builder.setView(R.layout.view_help)
74 | builder.create().show()
75 | }
76 |
77 | settingsToggle.setOnClickListener {
78 | optionsInnerContainer.visibility = if (optionsInnerContainer.visibility == View.GONE) VISIBLE else GONE
79 | }
80 |
81 | openImage.setOnClickListener {
82 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
83 | ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_OPEN_IMAGE_REQUEST_CODE)
84 | } else {
85 | openImageAndSetup()
86 | }
87 | }
88 |
89 | openMusic.setOnClickListener {
90 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
91 | || ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
92 | ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO), PERMISSION_OPEN_MUSIC_REQUEST_CODE)
93 | } else {
94 | openMusicAndSetup()
95 | }
96 | }
97 |
98 | viewOnGitHub.setOnClickListener {
99 | val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/SumiMakito/RhythmView"))
100 | startActivity(browserIntent)
101 | }
102 |
103 | spinningSpeed.progress = (rhythmView.coverSpinningSpeed * 10).toInt() + 30
104 | spinningSpeedDisplay.text = "${rhythmView.coverSpinningSpeed}"
105 | spinningSpeed.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
106 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
107 | if (!fromUser) return
108 | rhythmView.coverSpinningSpeed = (progress - 30) / 10f
109 | spinningSpeedDisplay.text = "${rhythmView.coverSpinningSpeed}"
110 | }
111 |
112 | override fun onStartTrackingTouch(seekBar: SeekBar?) {
113 | }
114 |
115 | override fun onStopTrackingTouch(seekBar: SeekBar?) {
116 |
117 | }
118 | })
119 |
120 | innerPadding.progress = (rhythmView.innerDrawingPaddingScale * 100).toInt()
121 | innerPaddingDisplay.text = "${rhythmView.innerDrawingPaddingScale}"
122 | innerPadding.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
123 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
124 | if (!fromUser) return
125 | rhythmView.innerDrawingPaddingScale = progress / 100f
126 | innerPaddingDisplay.text = "${rhythmView.innerDrawingPaddingScale}"
127 | }
128 |
129 | override fun onStartTrackingTouch(seekBar: SeekBar?) {
130 | }
131 |
132 | override fun onStopTrackingTouch(seekBar: SeekBar?) {
133 |
134 | }
135 | })
136 |
137 | outerDrawingSize.progress = (rhythmView.maxDrawingWidthScale * 100).toInt()
138 | outerDrawingSizeDisplay.text = "${rhythmView.maxDrawingWidthScale}"
139 | outerDrawingSize.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
140 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
141 | if (!fromUser) return
142 | rhythmView.maxDrawingWidthScale = progress / 100f
143 | outerDrawingSizeDisplay.text = "${rhythmView.maxDrawingWidthScale}"
144 | }
145 |
146 | override fun onStartTrackingTouch(seekBar: SeekBar?) {
147 | }
148 |
149 | override fun onStopTrackingTouch(seekBar: SeekBar?) {
150 |
151 | }
152 | })
153 |
154 | division.progress = divisionValue - 4
155 | divisionDisplay.text = "$divisionValue"
156 | division.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
157 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
158 | if (!fromUser) return
159 | divisionValue = progress + 4
160 | divisionDisplay.text = "$divisionValue"
161 | reloadVisualEffect()
162 | }
163 |
164 | override fun onStartTrackingTouch(seekBar: SeekBar?) {
165 | }
166 |
167 | override fun onStopTrackingTouch(seekBar: SeekBar?) {
168 |
169 | }
170 | })
171 |
172 | waveSpeed.progress = (waveSpeedValue * 1000).toInt()
173 | waveSpeedDisplay.text = "$waveSpeedValue"
174 | waveSpeed.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
175 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
176 | if (!fromUser) return
177 | waveSpeedValue = progress / 1000f
178 | waveSpeedDisplay.text = "$waveSpeedValue"
179 | reloadVisualEffect()
180 | }
181 |
182 | override fun onStartTrackingTouch(seekBar: SeekBar?) {
183 | }
184 |
185 | override fun onStopTrackingTouch(seekBar: SeekBar?) {
186 |
187 | }
188 | })
189 |
190 | particleSpeed.progress = (particleSpeedValue * 1000).toInt() - 5
191 | particleSpeedDisplay.text = "$particleSpeedValue"
192 | particleSpeed.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
193 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
194 | if (!fromUser) return
195 | particleSpeedValue = (progress + 5) / 1000f
196 | particleSpeedDisplay.text = "$particleSpeedValue"
197 | reloadVisualEffect()
198 | }
199 |
200 | override fun onStartTrackingTouch(seekBar: SeekBar?) {
201 | }
202 |
203 | override fun onStopTrackingTouch(seekBar: SeekBar?) {
204 |
205 | }
206 | })
207 |
208 | visualEffect.setOnCheckedChangeListener { _, checkedId ->
209 | when (checkedId) {
210 | R.id.radioButtonRipple -> {
211 | divisionValue = 16
212 | reloadVisualEffect()
213 | waveSpeedValue = 0.04f
214 | particleSpeedValue = 0.005f
215 | waveSpeed.progress = (waveSpeedValue * 1000).toInt()
216 | division.max = 12
217 | division.progress = divisionValue - 4
218 | waveSpeedDisplay.text = "$waveSpeedValue"
219 | divisionDisplay.text = "$divisionValue"
220 | particleSpeed.progress = (particleSpeedValue * 1000).toInt() - 5
221 | particleSpeedDisplay.text = "$particleSpeedValue"
222 | particleSpeed.isEnabled = true
223 | }
224 | R.id.radioButtonRay -> {
225 | divisionValue = 256
226 | waveSpeedValue = 0.04f
227 | waveSpeed.progress = (waveSpeedValue * 1000).toInt()
228 | division.max = 252
229 | division.progress = divisionValue - 4
230 | waveSpeedDisplay.text = "$waveSpeedValue"
231 | divisionDisplay.text = "$divisionValue"
232 | particleSpeed.isEnabled = false
233 | }
234 | R.id.radioButtonRainbowRay -> {
235 | divisionValue = 360
236 | waveSpeedValue = 0.04f
237 | waveSpeed.progress = (waveSpeedValue * 1000).toInt()
238 | division.max = 356
239 | division.progress = divisionValue - 4
240 | waveSpeedDisplay.text = "$waveSpeedValue"
241 | divisionDisplay.text = "$divisionValue"
242 | particleSpeed.isEnabled = false
243 | }
244 | }
245 | reloadVisualEffect()
246 | }
247 | }
248 |
249 | private fun openMusicAndSetup() {
250 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
251 | intent.addCategory(Intent.CATEGORY_OPENABLE)
252 | intent.type = "audio/*"
253 | startActivityForResult(intent, OPEN_MUSIC_REQUEST_CODE);
254 | }
255 |
256 | private fun openImageAndSetup() {
257 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
258 | intent.addCategory(Intent.CATEGORY_OPENABLE)
259 | intent.type = "image/*"
260 | startActivityForResult(intent, OPEN_IMAGE_REQUEST_CODE);
261 | }
262 |
263 | private fun updateColor(bitmap: Bitmap) {
264 | colorM = getDominantColor(bitmap)
265 | val hsvL = FloatArray(3)
266 | val hsvM = FloatArray(3)
267 | val hsvD = FloatArray(3)
268 | Color.colorToHSV(colorM, hsvL)
269 | Color.colorToHSV(colorM, hsvM)
270 | Color.colorToHSV(colorM, hsvD)
271 | hsvL[1] = max(0.2f, min(0.3f, hsvL[1] * 0.20f))
272 | hsvM[1] = max(0.2f, min(0.3f, hsvM[1] * 0.20f))
273 | hsvD[1] = max(0.2f, min(0.3f, hsvD[1] * 0.20f))
274 | hsvL[2] = 0.98f
275 | hsvM[2] = 0.92f
276 | hsvD[2] = 0.86f
277 | colorH = Color.HSVToColor(hsvL)
278 | colorM = Color.HSVToColor(hsvM)
279 | colorL = Color.HSVToColor(hsvD)
280 | }
281 |
282 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
283 | when (requestCode) {
284 | PERMISSION_OPEN_MUSIC_REQUEST_CODE -> {
285 | if (grantResults.isEmpty()) return
286 | for (result in grantResults) {
287 | if (result != PackageManager.PERMISSION_GRANTED) return
288 | }
289 | openMusicAndSetup()
290 | }
291 | PERMISSION_OPEN_IMAGE_REQUEST_CODE -> {
292 | if (grantResults.isEmpty()) return
293 | for (result in grantResults) {
294 | if (result != PackageManager.PERMISSION_GRANTED) return
295 | }
296 | openImageAndSetup()
297 | }
298 | }
299 | }
300 |
301 |
302 | public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
303 | if (resultCode == Activity.RESULT_OK && data != null && data.data != null) {
304 | when (requestCode) {
305 | OPEN_MUSIC_REQUEST_CODE -> {
306 | try {
307 | if (mediaPlayer != null) mediaPlayer!!.stop()
308 | mediaPlayer = MediaPlayer.create(this, data.data)
309 | mediaPlayer!!.isLooping = true
310 | mediaPlayer!!.start()
311 | reloadVisualEffect()
312 | } catch (e: Exception) {
313 | e.printStackTrace()
314 | return
315 | }
316 | }
317 | OPEN_IMAGE_REQUEST_CODE -> {
318 | try {
319 | val coverImage = ContentHelper.absolutePathFromUri(this, data.data)
320 | if (coverImage != null) {
321 | rhythmView.albumCover = BitmapFactory.decodeFile(coverImage)
322 | updateColor(rhythmView.albumCover!!)
323 | reloadVisualEffect()
324 | }
325 | } catch (e: Exception) {
326 | e.printStackTrace()
327 | return
328 | }
329 | }
330 | }
331 | } else
332 | super.onActivityResult(requestCode, resultCode, data)
333 | }
334 |
335 | private fun getDominantColor(bitmap: Bitmap): Int {
336 | val newBitmap = Bitmap.createScaledBitmap(bitmap, 8, 8, true)
337 | var red = 0
338 | var green = 0
339 | var blue = 0
340 | var c = 0
341 | var r: Int
342 | var g: Int
343 | var b: Int
344 | for (y in 0 until newBitmap.getHeight()) {
345 | for (x in 0 until newBitmap.getHeight()) {
346 | val color = newBitmap.getPixel(x, y)
347 | r = color shr 16 and 0xFF
348 | g = color shr 8 and 0xFF
349 | b = color and 0xFF
350 | if (r > 200 || g > 200 || b > 200) continue
351 | red += r
352 | green += g
353 | blue += b
354 | c++
355 | }
356 | }
357 | newBitmap.recycle()
358 | if (c == 0) {
359 | return 0xFFFFFFFF.toInt()
360 | } else {
361 | red = Math.max(0, Math.min(0xFF, red / c))
362 | green = Math.max(0, Math.min(0xFF, green / c))
363 | blue = Math.max(0, Math.min(0xFF, blue / c))
364 |
365 | val hsv = FloatArray(3)
366 | Color.RGBToHSV(red, green, blue, hsv)
367 | hsv[2] = Math.max(hsv[2], 0.7f)
368 |
369 | return 0xFF shl 24 or Color.HSVToColor(hsv)
370 | }
371 | }
372 |
373 | private fun reloadVisualEffect() {
374 | if (mediaPlayer == null) return
375 | /*
376 | if (mediaPlayer == null) {
377 | mediaPlayer = MediaPlayer.create(this, R.raw.creativeminds)
378 | mediaPlayer!!.isLooping = true
379 | mediaPlayer!!.start()
380 | }
381 | */
382 |
383 | when (visualEffect.checkedRadioButtonId) {
384 | R.id.radioButtonRipple -> {
385 | dataSource = PlaybackSource(mediaPlayer!!, 3 * divisionValue)
386 | val ripple = Ripple(rhythmView, divisionValue, waveSpeedValue, particleSpeedValue)
387 | // ripple.colorLF = colorL
388 | // ripple.colorMF = colorM
389 | // ripple.colorHF = colorH
390 | // ripple.colorParticle = colorM
391 | ripple.dataSource = dataSource
392 | rhythmView.visualEffect = ripple
393 | }
394 | R.id.radioButtonRay -> {
395 | dataSource = PlaybackSource(mediaPlayer!!, 3 * divisionValue)
396 | val ray = Ray(rhythmView, divisionValue, waveSpeedValue)
397 | ray.dataSource = dataSource
398 | rhythmView.visualEffect = ray
399 | }
400 | R.id.radioButtonRainbowRay -> {
401 | dataSource = PlaybackSource(mediaPlayer!!, divisionValue)
402 | val rayMono = RainbowRay(rhythmView, divisionValue, waveSpeedValue)
403 | rayMono.dataSource = dataSource
404 | rhythmView.visualEffect = rayMono
405 | }
406 | }
407 | }
408 |
409 | override fun onDestroy() {
410 | super.onDestroy()
411 | mediaPlayer?.stop()
412 | mediaPlayer?.release()
413 | }
414 |
415 | override fun onPause() {
416 | super.onPause()
417 | rhythmView?.isPaused = true
418 | if (mediaPlayer != null && mediaPlayer!!.isPlaying) {
419 | mediaPlayer!!.pause()
420 | }
421 | }
422 |
423 | override fun onResume() {
424 | super.onResume()
425 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
426 | || ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
427 | startActivity(Intent(this, PromptActivity::class.java))
428 | this.finish()
429 | return
430 | }
431 | rhythmView?.isPaused = false
432 | if (mediaPlayer != null && !mediaPlayer!!.isPlaying) {
433 | mediaPlayer!!.start()
434 | }
435 | }
436 | }
437 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/github/sumimakito/rhythmviewdemo/PromptActivity.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmviewdemo
2 |
3 | import android.Manifest
4 | import android.content.Intent
5 | import android.content.pm.PackageManager
6 | import android.os.Bundle
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.core.app.ActivityCompat
9 | import androidx.core.content.ContextCompat
10 | import kotlinx.android.synthetic.main.activity_prompt.*
11 |
12 | class PromptActivity : AppCompatActivity() {
13 | companion object {
14 | private const val PERMISSION_REQUEST_CODE = 0xCAE
15 | }
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | setContentView(R.layout.activity_prompt)
20 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
21 | && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
22 | startActivity(Intent(this, MainActivity::class.java))
23 | this.finish()
24 | return
25 | }
26 | continueButton.setOnClickListener {
27 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
28 | || ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
29 | ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO), PERMISSION_REQUEST_CODE)
30 | } else {
31 | startActivity(Intent(this, MainActivity::class.java))
32 | this.finish()
33 | }
34 | }
35 | }
36 |
37 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
38 | when (requestCode) {
39 | PERMISSION_REQUEST_CODE -> {
40 | if (grantResults.isEmpty()) return
41 | for (result in grantResults) {
42 | if (result != PackageManager.PERMISSION_GRANTED) return
43 | }
44 | startActivity(Intent(this, MainActivity::class.java))
45 | this.finish()
46 | }
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_check_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_help_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
46 |
51 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_library_music_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_photo_library_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_settings_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
23 |
24 |
29 |
30 |
37 |
38 |
43 |
44 |
52 |
53 |
58 |
59 |
60 |
67 |
68 |
69 |
70 |
75 |
76 |
84 |
85 |
90 |
91 |
92 |
99 |
100 |
101 |
102 |
103 |
108 |
109 |
117 |
118 |
123 |
124 |
131 |
132 |
133 |
134 |
139 |
140 |
148 |
149 |
154 |
155 |
162 |
163 |
164 |
165 |
170 |
171 |
179 |
180 |
185 |
186 |
193 |
194 |
195 |
196 |
201 |
202 |
209 |
210 |
217 |
218 |
224 |
225 |
231 |
232 |
233 |
237 |
238 |
243 |
244 |
252 |
253 |
258 |
259 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
290 |
291 |
303 |
304 |
316 |
317 |
329 |
330 |
331 |
345 |
346 |
352 |
353 |
359 |
360 |
361 |
362 |
373 |
374 |
380 |
381 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_prompt.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
18 |
19 |
25 |
26 |
31 |
32 |
38 |
39 |
44 |
45 |
50 |
51 |
57 |
58 |
63 |
64 |
70 |
71 |
76 |
77 |
83 |
84 |
90 |
91 |
99 |
100 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/view_help.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
19 |
20 |
27 |
28 |
34 |
35 |
40 |
41 |
47 |
48 |
53 |
54 |
55 |
56 |
62 |
63 |
68 |
69 |
75 |
76 |
81 |
82 |
83 |
84 |
85 |
90 |
91 |
96 |
97 |
103 |
104 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/raw/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/demo/src/main/res/raw/cover.jpg
--------------------------------------------------------------------------------
/demo/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #ba68c8
4 | #883997
5 | #FFFFFF
6 |
7 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #BA68C8
4 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RhythmView
3 | In order to demonstrate
4 | permission to work. But don\'t worry, we won\'t collect any of your personal data. Besides, this app is shipped without INTERNET permission.
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/src/main/res/xml/file_provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/demo/src/test/java/com/github/sumimakito/rhythmviewdemo/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmviewdemo
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sumimakito/RhythmView/661584b7119aa1b33e1eb0294ea72fd5f68ba3de/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/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 --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --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 --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/library/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | library
4 | Project library created by Buildship.
5 |
6 |
7 |
8 |
9 | org.eclipse.jdt.core.javabuilder
10 |
11 |
12 |
13 |
14 | org.eclipse.buildship.core.gradleprojectbuilder
15 |
16 |
17 |
18 |
19 |
20 | org.eclipse.jdt.core.javanature
21 | org.eclipse.buildship.core.gradleprojectnature
22 |
23 |
24 |
--------------------------------------------------------------------------------
/library/.settings/org.eclipse.buildship.core.prefs:
--------------------------------------------------------------------------------
1 | connection.project.dir=..
2 | eclipse.preferences.version=1
3 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | android {
8 | compileSdkVersion 28
9 |
10 |
11 |
12 | defaultConfig {
13 | minSdkVersion 9
14 | targetSdkVersion 28
15 | versionCode 1
16 | versionName "1.0.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 |
20 | }
21 |
22 | buildTypes {
23 | release {
24 | minifyEnabled false
25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
26 | }
27 | }
28 |
29 | }
30 |
31 | dependencies {
32 | implementation fileTree(dir: 'libs', include: ['*.jar'])
33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
34 | testImplementation 'junit:junit:4.12'
35 | androidTestImplementation 'androidx.test:runner:1.1.0-alpha3'
36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha3'
37 | }
38 |
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/library/src/androidTest/java/com/github/sumimakito/rhythmview/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview
2 |
3 | import androidx.test.InstrumentationRegistry
4 | import androidx.test.runner.AndroidJUnit4
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * @see [Testing documentation](http://d.android.com/tools/testing)
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getTargetContext()
20 |
21 | assertEquals("com.github.sumimakito.library.test", appContext.packageName)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/RhythmView.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview
2 |
3 | import android.annotation.SuppressLint
4 | import android.annotation.TargetApi
5 | import android.content.Context
6 | import android.graphics.*
7 | import android.os.Build
8 | import android.util.AttributeSet
9 | import android.view.View
10 | import com.github.sumimakito.rhythmview.effect.BaseEffect
11 | import java.text.DecimalFormat
12 | import kotlin.math.min
13 | import kotlin.math.round
14 |
15 | /**
16 | * A view for visualizing rhythms.
17 | */
18 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
19 | class RhythmView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0)
20 | : View(context, attrs, defStyleAttr, defStyleRes) {
21 |
22 | private var paintText = Paint()
23 | private var paintBitmap = Paint()
24 | private var paintParticle = Paint()
25 | private var coverRotation: Float = 0f
26 | private var fps = 0f
27 | private var frames = 0
28 | private var frameTime: Long = 0
29 | private var coverSourceRect: Rect? = null
30 | private var coverTargetRect: Rect? = null
31 | private var renderLoopStarted: Boolean = false
32 | private var renderInterval = 15L
33 | internal var centerX: Float = 0f
34 | internal var centerY: Float = 0f
35 | internal var radius: Float = 0f
36 | internal var minDrawingRadius: Float = 0f
37 | internal var maxDrawingWidth: Float = 70f
38 |
39 | /**
40 | * Speed of the spinning cover.
41 | * Unit: degree
42 | */
43 | var coverSpinningSpeed: Float = 0.5f
44 |
45 | /**
46 | * If true, the cover will stop spinning.
47 | */
48 | var isPaused: Boolean = true
49 |
50 | /**
51 | * If true, an FPS counter will show up at the top left corner of the view.
52 | */
53 | var showFpsCounter: Boolean = false
54 | var innerDrawingPaddingScale: Float = 0.01f
55 | set(value) {
56 | field = value
57 | updateLayoutMetrics()
58 | }
59 |
60 | var maxDrawingWidthScale = 0.24f
61 | set(value) {
62 | field = value
63 | updateLayoutMetrics()
64 | }
65 |
66 | var onRhythmViewLayoutChangedListener: (RhythmView)->Unit = {_ -> }
67 |
68 | private var scaledAlbumCover: Bitmap? = null
69 | var albumCover: Bitmap? = null
70 | set(value) {
71 | field = value
72 | if (value == null) return
73 | val clippingPaint = Paint()
74 | clippingPaint.color = 0xFFFFFFFF.toInt()
75 | clippingPaint.isAntiAlias = true
76 | val size = min(value.width, value.height)
77 | val squareRaw = Bitmap.createScaledBitmap(value, size, size, true)
78 | field = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
79 | val canvas = Canvas(albumCover)
80 | canvas.drawCircle((albumCover!!.width / 2).toFloat(), (albumCover!!.height / 2).toFloat(), (albumCover!!.width / 2).toFloat(), clippingPaint)
81 | clippingPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
82 | canvas.drawBitmap(squareRaw, 0f, 0f, clippingPaint)
83 | scaledAlbumCover = null
84 | }
85 |
86 | var visualEffect: BaseEffect<*>? = null
87 |
88 | init {
89 | paintText.color = Color.WHITE
90 | paintText.textSize = 36f
91 | paintText.alpha = 180
92 | paintText.isAntiAlias = true
93 |
94 | paintBitmap.color = Color.WHITE
95 | paintBitmap.isAntiAlias = true
96 | paintBitmap.isFilterBitmap = true
97 |
98 | paintParticle.color = Color.WHITE
99 | paintParticle.style = Paint.Style.FILL
100 | paintParticle.isAntiAlias = true
101 | }
102 |
103 | private fun updateLayoutMetrics() {
104 | val innerWidth = width - paddingLeft - paddingRight
105 | val innerHeight = height - paddingTop - paddingBottom
106 | val innerSize = Math.min(innerWidth, innerHeight)
107 | centerX = paddingLeft + innerWidth * .5f
108 | centerY = paddingTop + innerHeight * .5f
109 | maxDrawingWidth = innerSize * maxDrawingWidthScale / 2f
110 | radius = (innerSize - 2 * maxDrawingWidth - 2 * innerDrawingPaddingScale * innerSize) / 2f
111 | minDrawingRadius = radius + innerDrawingPaddingScale * innerSize
112 | coverTargetRect = null
113 |
114 | onRhythmViewLayoutChangedListener.invoke(this)
115 | }
116 |
117 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
118 | super.onLayout(changed, left, top, right, bottom)
119 |
120 | updateLayoutMetrics()
121 |
122 | if (!renderLoopStarted) {
123 | renderLoopStarted = true
124 | post(object : Runnable {
125 | override fun run() {
126 | if (visualEffect != null && visualEffect!!.dataSource != null) {
127 | invalidate()
128 | }
129 | postDelayed(this, renderInterval)
130 | }
131 | })
132 | }
133 | }
134 |
135 | @SuppressLint("DrawAllocation")
136 | override fun onDraw(canvas: Canvas?) {
137 | super.onDraw(canvas)
138 |
139 | if (showFpsCounter) {
140 | val df = DecimalFormat("0.00")
141 | canvas?.drawText("${df.format(fps)} f/s", 18f, 18f + paintText.textSize, paintText)
142 | }
143 |
144 | visualEffect?.render(canvas!!)
145 |
146 | if (albumCover != null) {
147 | canvas?.save()
148 | canvas?.rotate(coverRotation, centerX, centerY)
149 | if (coverTargetRect == null) {
150 | coverTargetRect = Rect(round(centerX - radius).toInt(), round(centerY - radius).toInt(), round(centerX + radius).toInt(), round(centerY + radius).toInt())
151 | }
152 | /**
153 | * Cache the scaled album cover for a better performance.
154 | */
155 | if (scaledAlbumCover == null) {
156 | canvas?.drawBitmap(albumCover, Rect(0, 0, albumCover!!.width, albumCover!!.height), coverTargetRect, paintBitmap)
157 | Thread {
158 | scaledAlbumCover = Bitmap.createScaledBitmap(albumCover, coverTargetRect!!.width(), coverTargetRect!!.height(), true)
159 | coverSourceRect = Rect(0, 0, scaledAlbumCover!!.width, scaledAlbumCover!!.height)
160 | }.start()
161 | } else {
162 | canvas?.drawBitmap(scaledAlbumCover, coverSourceRect, coverTargetRect, paintBitmap)
163 | }
164 | canvas?.restore()
165 | }
166 |
167 | visualEffect?.onFrameRendered()
168 |
169 | if (!isPaused) {
170 | coverRotation += coverSpinningSpeed
171 | }
172 | if (coverRotation >= 360f) {
173 | coverRotation = 0f
174 | }
175 |
176 | if (showFpsCounter) {
177 | val time = System.currentTimeMillis()
178 | val delta = time - frameTime
179 | if (delta > 1000) {
180 | frameTime = time
181 | fps = (frames.toFloat() / delta * 1000)
182 | frames = 0
183 | } else {
184 | frames++
185 | }
186 | }
187 | }
188 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/datasource/BaseDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview.datasource
2 |
3 | /**
4 | * Base class for data sources.
5 | *
6 | * You may create a customized data source by extending this class.
7 | *
8 | * `resolution` defines the number of items in this data source.
9 | */
10 | abstract class BaseDataSource(val resolution: Int) {
11 | var data: Array? = null
12 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/datasource/PlaybackSource.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview.datasource
2 |
3 | import android.media.MediaPlayer
4 | import android.media.audiofx.Visualizer
5 | import android.util.Log
6 | import kotlin.math.floor
7 | import kotlin.math.max
8 | import kotlin.math.min
9 |
10 | /**
11 | * A preset data source.
12 | *
13 | * Extract waveform data from the given media file using `MediaPlayer`.
14 | *
15 | * NOTICE: this data source requires the RECORD_AUDIO permission to work.
16 | */
17 | class PlaybackSource(private val mediaPlayer: MediaPlayer, resolution: Int) : BaseDataSource(resolution) {
18 | companion object {
19 | private const val TAG = "PlaybackSource"
20 | }
21 |
22 | private var visualizer: Visualizer = Visualizer(mediaPlayer.audioSessionId)
23 |
24 | init {
25 | visualizer.enabled = false
26 | var captureSize = 2
27 | while (captureSize < resolution) {
28 | captureSize *= 2
29 | }
30 | if (captureSize > Visualizer.getCaptureSizeRange()[1]) {
31 | /**
32 | * This can cause an app crash.
33 | */
34 | Log.e(TAG, "Capture size $captureSize exceeded the maximum capture size: ${Visualizer.getCaptureSizeRange()[1]}. Will use the maximum capture size instead.")
35 | visualizer.captureSize = Visualizer.getCaptureSizeRange()[1]
36 | } else {
37 | visualizer.captureSize = captureSize
38 | }
39 | visualizer.setDataCaptureListener(object : Visualizer.OnDataCaptureListener {
40 | override fun onFftDataCapture(visualizer: Visualizer?, bytes: ByteArray?, samplingRate: Int) {
41 |
42 | }
43 |
44 | override fun onWaveFormDataCapture(p0: Visualizer?, bytes: ByteArray?, p2: Int) {
45 | val wave = Array(resolution) { 0 }
46 | val samplingInterval = max(1, floor((bytes!!.size - 1).toFloat() / wave.size).toInt())
47 | for (i in 0 until wave.size) {
48 | /**
49 | * Shift and clamp
50 | */
51 | wave[i] = max(0, min(256, bytes[i * samplingInterval] + 128))
52 | }
53 | data = wave
54 | }
55 | }, Visualizer.getMaxCaptureRate(), true, false)
56 | visualizer.enabled = true
57 | }
58 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/effect/BaseEffect.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview.effect
2 |
3 | import android.graphics.Canvas
4 | import com.github.sumimakito.rhythmview.RhythmView
5 | import com.github.sumimakito.rhythmview.datasource.BaseDataSource
6 |
7 | /**
8 | * Base class for visual effects.
9 | *
10 | * You may create a customized visual effect by extending this class.
11 | *
12 | * `T` should be the type provided by the corresponding data source.
13 | */
14 | abstract class BaseEffect(val centerX: Float, val centerY: Float, val radius: Float, var minDrawingRadius: Float, var maxDrawingWidth: Float, var dataSource: BaseDataSource? = null) {
15 | constructor(rhythmView: RhythmView, dataSource: BaseDataSource? = null) : this(rhythmView.centerX, rhythmView.centerY, rhythmView.radius, rhythmView.minDrawingRadius, rhythmView.maxDrawingWidth, dataSource)
16 |
17 | /**
18 | * Called when a frame is being rendered.
19 | *
20 | * You should do rendering works on `canvas` in this function.
21 | */
22 | abstract fun render(canvas: Canvas)
23 |
24 | /**
25 | * Called when a frame has been rendered.
26 | *
27 | * You should call `nextTick()` functions in this function.
28 | */
29 | abstract fun onFrameRendered()
30 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/effect/RainbowRay.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview.effect
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Color
5 | import android.graphics.Paint
6 | import android.graphics.PointF
7 | import com.github.sumimakito.rhythmview.RhythmView
8 | import com.github.sumimakito.rhythmview.util.MathUtils
9 | import com.github.sumimakito.rhythmview.wave.WavePoint
10 | import java.util.*
11 | import kotlin.math.floor
12 | import kotlin.math.max
13 | import kotlin.math.min
14 |
15 | /**
16 | * A preset visual effect.
17 | *
18 | * Resolution of the data source should not be less than `resolution` here.
19 | *
20 | * When using with `PlaybackSource`, parameter `resolution` should not be larger than 1024, or the
21 | * capture size may exceeded the maximum capture size of the system.
22 | */
23 | class RainbowRay @JvmOverloads constructor(rhythmView: RhythmView, private val resolution: Int = 256, private val waveSpeed: Float = 0.04f) : BaseEffect(rhythmView) {
24 |
25 | var color: Int = 0xffef9a9a.toInt()
26 | var alpha: Float = 0.65f
27 | private var frameId = 0
28 | private var wavePoints = ArrayList()
29 | private var outerPoints = ArrayList()
30 | private var innerPoints = ArrayList()
31 | private var paintRay = Paint()
32 | private var delta: Float
33 | private val hsv = FloatArray(3)
34 |
35 | init {
36 | if (resolution < 4) throw RuntimeException("Division should be an integer larger than 4.")
37 | delta = 360f / resolution
38 | for (i in 0 until resolution) {
39 | wavePoints.add(WavePoint(0f, waveSpeed, 0f, 1f))
40 | }
41 |
42 | paintRay.isAntiAlias = true
43 | paintRay.style = Paint.Style.STROKE
44 | paintRay.strokeWidth = 3f
45 |
46 | computePoints()
47 | }
48 |
49 | private fun refillWave(wave: Array) {
50 | for (i in 0 until min(wavePoints.size, wave.size)) {
51 | wavePoints[i].changeTo(wave[i] / 256f)
52 | }
53 | }
54 |
55 | override fun onFrameRendered() {
56 | if (frameId % 2 == 0) {
57 | if (dataSource != null && dataSource!!.data != null) refillWave(dataSource!!.data!!)
58 | }
59 |
60 | for (wavePoint in wavePoints) {
61 | wavePoint.nextTick()
62 | }
63 | computePoints()
64 |
65 | frameId++
66 | if (frameId > 2) {
67 | frameId = 0
68 | }
69 | }
70 |
71 | override fun render(canvas: Canvas) {
72 | paintRay.alpha = floor(255f * alpha).toInt()
73 |
74 | for (i in 0 until resolution) {
75 | val start = innerPoints[i]
76 | val stop = outerPoints[i]
77 | canvas.drawLine(start.x, start.y, stop.x, stop.y, paintRay)
78 |
79 | /**
80 | * Apply color magic! Rua!
81 | */
82 | Color.colorToHSV(color, hsv)
83 | hsv[0] += max(1f, i.toFloat() / resolution * 360f)
84 | while (hsv[0] >= 360) hsv[0] -= 360f
85 | paintRay.color = Color.HSVToColor(hsv)
86 | }
87 | }
88 |
89 | private fun computePoints() {
90 | outerPoints.clear()
91 | for (i in 0 until wavePoints.size) {
92 | val deg = (i % 360) * delta
93 | innerPoints.add(MathUtils.getPointOnCircle(centerX, centerY, minDrawingRadius, deg))
94 | outerPoints.add(MathUtils.getPointOnCircle(centerX, centerY, minDrawingRadius + getWaveHeight(i), deg))
95 | }
96 | }
97 |
98 | private fun getWaveHeight(index: Int): Float {
99 | if (index < wavePoints.size) {
100 | return min(1f, max(0f, wavePoints[index].displayValue)) * maxDrawingWidth
101 | }
102 | return 0f
103 | }
104 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/effect/Ray.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview.effect
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Paint
5 | import android.graphics.PointF
6 | import com.github.sumimakito.rhythmview.RhythmView
7 | import com.github.sumimakito.rhythmview.util.MathUtils
8 | import com.github.sumimakito.rhythmview.wave.WavePoint
9 | import kotlin.math.floor
10 | import kotlin.math.max
11 | import kotlin.math.min
12 |
13 | /**
14 | * A preset visual effect.
15 | *
16 | * Resolution of the data source should not be less than `resolution * 3` here.
17 | *
18 | * When using with `PlaybackSource`, parameter `resolution` should not be larger than 256, or the
19 | * capture size may exceeded the maximum capture size of the system.
20 | */
21 | class Ray @JvmOverloads constructor(rhythmView: RhythmView, private val resolution: Int = 256, private val waveSpeed: Float = 0.04f) : BaseEffect(rhythmView) {
22 | var colorHF: Int = 0xffef9a9a.toInt()
23 | var colorMF: Int = 0xff90caf9.toInt()
24 | var colorLF: Int = 0xffa5d6a7.toInt()
25 | var alphaHF: Float = 0.65f
26 | var alphaMF: Float = 0.65f
27 | var alphaLF: Float = 0.65f
28 | private var frameId = 0
29 | private var wavePoints = ArrayList()
30 | private var outerPoints = ArrayList()
31 | private var innerPoints = ArrayList()
32 | private var paintRay = Paint()
33 | private var delta: Float
34 |
35 | init {
36 | if (resolution < 4) throw RuntimeException("Division should be an integer larger than 4.")
37 | delta = 360f / resolution
38 | for (i in 0 until resolution * 3) {
39 | wavePoints.add(WavePoint(0f, waveSpeed, 0f, 1f))
40 | }
41 |
42 | paintRay.isAntiAlias = true
43 | paintRay.style = Paint.Style.STROKE
44 | paintRay.strokeWidth = 3f
45 |
46 | computePoints()
47 | }
48 |
49 | private fun refillWave(wave: Array) {
50 | for (i in 0 until min(wavePoints.size, wave.size)) {
51 | wavePoints[i].changeTo(wave[i] / 256f)
52 | }
53 | }
54 |
55 | override fun onFrameRendered() {
56 | if (frameId % 2 == 0) {
57 | if (dataSource != null && dataSource!!.data != null) refillWave(dataSource!!.data!!)
58 | }
59 |
60 | for (wavePoint in wavePoints) {
61 | wavePoint.nextTick()
62 | }
63 | computePoints()
64 |
65 | frameId++
66 | if (frameId > 2) {
67 | frameId = 0
68 | }
69 | }
70 |
71 | override fun render(canvas: Canvas) {
72 | paintRay.color = colorHF
73 | paintRay.alpha = floor(255f * alphaHF).toInt()
74 |
75 | var ptIndex = 0
76 | for (i in 0 until resolution) {
77 | val start = innerPoints[ptIndex]
78 | val stop = outerPoints[ptIndex]
79 | canvas.drawLine(start.x, start.y, stop.x, stop.y, paintRay)
80 | ptIndex++
81 | }
82 |
83 | paintRay.color = colorLF
84 | paintRay.alpha = floor(255f * alphaLF).toInt()
85 | for (i in 0 until resolution) {
86 | val start = innerPoints[ptIndex]
87 | val stop = outerPoints[ptIndex]
88 | canvas.drawLine(start.x, start.y, stop.x, stop.y, paintRay)
89 | ptIndex++
90 | }
91 |
92 | paintRay.color = colorMF
93 | paintRay.alpha = floor(255f * alphaMF).toInt()
94 | for (i in 0 until resolution) {
95 | val start = innerPoints[ptIndex]
96 | val stop = outerPoints[ptIndex]
97 | canvas.drawLine(start.x, start.y, stop.x, stop.y, paintRay)
98 | ptIndex++
99 | }
100 | }
101 |
102 | private fun computePoints() {
103 | outerPoints.clear()
104 | for (i in 0 until wavePoints.size) {
105 | val deg = (i % 360) * delta
106 | innerPoints.add(MathUtils.getPointOnCircle(centerX, centerY, minDrawingRadius, deg))
107 | outerPoints.add(MathUtils.getPointOnCircle(centerX, centerY, minDrawingRadius + getWaveHeight(i), deg))
108 | }
109 | }
110 |
111 | private fun getWaveHeight(index: Int): Float {
112 | if (index < wavePoints.size) {
113 | return min(1f, max(0f, wavePoints[index].displayValue)) * maxDrawingWidth
114 | }
115 | return 0f
116 | }
117 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/effect/Ripple.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview.effect
2 |
3 | import android.graphics.*
4 | import com.github.sumimakito.rhythmview.RhythmView
5 | import com.github.sumimakito.rhythmview.particle.Particle
6 | import com.github.sumimakito.rhythmview.particle.ParticleManager
7 | import com.github.sumimakito.rhythmview.util.MathUtils
8 | import com.github.sumimakito.rhythmview.wave.WavePoint
9 | import java.util.*
10 | import kotlin.math.floor
11 | import kotlin.math.max
12 | import kotlin.math.min
13 |
14 | /**
15 | * A preset visual effect.
16 | *
17 | * Resolution of the data source should not be less than `resolution * 3` here.
18 | *
19 | * Parameter `resolution` should not be larger than 16 or the edges may looks not smooth enough.
20 | *
21 | * When using with `PlaybackSource`, parameter `resolution` should not be larger than 256, or the
22 | * capture size may exceeded the maximum capture size of the system.
23 | */
24 | class Ripple @JvmOverloads constructor(rhythmView: RhythmView, private val resolution: Int = 8, private val waveSpeed: Float = 0.06f, private val particleSpeed: Float = 0.005f) : BaseEffect(rhythmView) {
25 | init {
26 | /**
27 | * Since the radius for the paths seems to be not that precise, in order to reveal the ripple
28 | * even if the playback has stopped, I applied a dirty-fix.
29 | */
30 | minDrawingRadius += maxDrawingWidth * 0.3f
31 | }
32 |
33 | var colorHF: Int = 0xFFFFFFFF.toInt()
34 | var colorMF: Int = 0xFFFFFFFF.toInt()
35 | var colorLF: Int = 0xFFFFFFFF.toInt()
36 | var colorParticle: Int = 0xFFFFFFFF.toInt()
37 | var alphaHF: Float = 0.2f
38 | var alphaMF: Float = 0.2f
39 | var alphaLF: Float = 0.2f
40 | private var frameId = 0
41 | private var curvePathHF = Path()
42 | private var curvePathMF = Path()
43 | private var curvePathLF = Path()
44 | private var wavePoints = ArrayList()
45 | private var curvePoints = ArrayList()
46 | private var paintRipple = Paint()
47 | private var paintParticle = Paint()
48 | private var delta: Float
49 | private var particleManager = ParticleManager()
50 | private var allZero: Boolean = true
51 |
52 | init {
53 | if (resolution < 4) throw RuntimeException("Division should be an integer larger than 4.")
54 | delta = 360f / resolution
55 | for (i in 0 until resolution * 3) {
56 | wavePoints.add(WavePoint(0f, waveSpeed, 0f, 1f))
57 | }
58 |
59 | particleManager.capacity = 200
60 |
61 | paintRipple.isAntiAlias = true
62 | paintRipple.pathEffect = CornerPathEffect(radius)
63 |
64 | paintParticle.color = Color.WHITE
65 | paintParticle.style = Paint.Style.FILL
66 | paintParticle.isAntiAlias = true
67 |
68 | computePoints()
69 | buildPath()
70 | }
71 |
72 | private fun refillWave(wave: Array) {
73 | var allZero = true
74 | for (i in 0 until min(wavePoints.size, wave.size)) {
75 | if (wave[i] != 0) allZero = false
76 | wavePoints[i].changeTo(wave[i] / 256f)
77 | }
78 | this.allZero = allZero
79 | }
80 |
81 | override fun onFrameRendered() {
82 | /**
83 | * Refill the waveform data every two frames to reduce the changing rate for the wave.
84 | */
85 | if (frameId % 2 == 0) {
86 | if (dataSource != null && dataSource!!.data != null) {
87 | refillWave(dataSource!!.data!!)
88 | }
89 | }
90 |
91 | /**
92 | * Create a new particle every 30 frames.
93 | *
94 | * If the playback stops (waveform has no non-zero items), stop creating new particles.
95 | */
96 | if (!allZero && frameId % 30 == 0) {
97 | val direction = (Math.random() * 360).toFloat()
98 | val startPoint = MathUtils.getPointOnCircle(centerX, centerY, minDrawingRadius * 0.5f, direction)
99 | particleManager.add(Particle(startPoint, direction, particleSpeed, minDrawingRadius * 0.5f + maxDrawingWidth * 0.4f))
100 | }
101 |
102 | particleManager.nextTick()
103 | for (wavePoint in wavePoints) {
104 | wavePoint.nextTick()
105 | }
106 | computePoints()
107 | buildPath()
108 |
109 | frameId++
110 | if (frameId > 60) {
111 | frameId = 0
112 | }
113 | }
114 |
115 | override fun render(canvas: Canvas) {
116 | paintRipple.color = colorHF
117 | paintRipple.alpha = floor(255f * alphaHF).toInt()
118 | canvas.drawPath(curvePathHF, paintRipple)
119 |
120 | paintRipple.color = colorLF
121 | paintRipple.alpha = floor(255f * alphaLF).toInt()
122 | canvas.drawPath(curvePathLF, paintRipple)
123 |
124 | paintRipple.color = colorMF
125 | paintRipple.alpha = floor(255f * alphaMF).toInt()
126 | canvas.drawPath(curvePathMF, paintRipple)
127 |
128 | paintParticle.color = colorParticle
129 | for (particle in particleManager.particles) {
130 | var particleRadius = 10f
131 | if (particle.life < 0.2) {
132 | particleRadius *= ((particle.life + 0.1f) / 0.3f)
133 | paintParticle.alpha = floor(180 * ((particle.life) / 0.2f)).toInt()
134 | } else {
135 | paintParticle.alpha = 180
136 | }
137 | canvas.drawCircle(particle.x, particle.y, particleRadius, paintParticle)
138 | }
139 | }
140 |
141 | private fun computePoints() {
142 | curvePoints.clear()
143 | for (i in 0 until wavePoints.size) {
144 | curvePoints.add(MathUtils.getPointOnCircle(centerX, centerY, minDrawingRadius + getWaveHeight(i), (i % 360) * delta))
145 | }
146 | }
147 |
148 | private fun buildPath() {
149 | curvePathLF.reset()
150 | curvePathLF.moveTo(curvePoints[0].x, curvePoints[0].y)
151 | for (i in 1 until resolution) {
152 | curvePathLF.lineTo(curvePoints[i].x, curvePoints[i].y)
153 | }
154 | curvePathLF.close()
155 |
156 | curvePathMF.reset()
157 | curvePathMF.moveTo(curvePoints[resolution].x, curvePoints[resolution].y)
158 | for (i in 1 until resolution) {
159 | curvePathMF.lineTo(curvePoints[resolution + i].x, curvePoints[resolution + i].y)
160 | }
161 | curvePathMF.close()
162 |
163 | curvePathHF.reset()
164 | curvePathHF.moveTo(curvePoints[resolution * 2].x, curvePoints[resolution * 2].y)
165 | for (i in 1 until resolution) {
166 | curvePathHF.lineTo(curvePoints[resolution * 2 + i].x, curvePoints[resolution * 2 + i].y)
167 | }
168 | curvePathHF.close()
169 | }
170 |
171 | private fun getWaveHeight(index: Int): Float {
172 | if (index < wavePoints.size) {
173 | return min(1f, max(0f, wavePoints[index].displayValue)) * maxDrawingWidth * 0.4f
174 | }
175 | return 0f
176 | }
177 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/particle/Particle.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview.particle
2 |
3 | import android.graphics.PointF
4 | import com.github.sumimakito.rhythmview.util.MathUtils
5 | import kotlin.math.cos
6 | import kotlin.math.sin
7 |
8 | /**
9 | * An abstract particle for the particle system.
10 | */
11 | class Particle(private val startX: Float, private val startY: Float, private val deg: Float, private val velocity: Float, private val lifespan: Float) {
12 | constructor(startPoint: PointF, direction: Float, velocity: Float, lifespan: Float) : this(startPoint.x, startPoint.y, direction, velocity, lifespan)
13 |
14 | companion object {
15 | /**
16 | * To avoid the loss of precision, a rather small threshold is set to recycle more particles.
17 | */
18 | private const val IGNORANCE_THRESHOLD = 0.0001f
19 | }
20 |
21 | private val deltaX = (lifespan * velocity * cos(MathUtils.deg2rad(deg.toDouble()))).toFloat()
22 | private val deltaY = -(lifespan * velocity * sin(MathUtils.deg2rad(deg.toDouble()))).toFloat()
23 |
24 | var recyclable: Boolean = false
25 | private set
26 |
27 | var x: Float = startX
28 | private set
29 |
30 | var y: Float = startY
31 | private set
32 |
33 | var life = 1f
34 | private set
35 |
36 | /**
37 | * Remember to call `nextTick()` after rendering each frame.
38 | *
39 | * If the particle is managed by `ParticleManager`, then call `nextTick()` in the manager instead.
40 | */
41 | fun nextTick() {
42 | life -= velocity
43 | if (life > IGNORANCE_THRESHOLD) {
44 | x += deltaX
45 | y += deltaY
46 | } else {
47 | recyclable = true
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/particle/ParticleManager.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview.particle
2 |
3 | import java.util.*
4 |
5 | /**
6 | * A manager for auto managing particles in the particle system.
7 | */
8 | class ParticleManager {
9 | val particles = LinkedList()
10 | /**
11 | * Capacity for the particle system.
12 | *
13 | * When the total number of particles exist in the system exceeds this value, earlier added
14 | * particles will be recycled first to ensure the performance.
15 | *
16 | * Set to a value less than zero to eliminate the limitation. (not recommended)
17 | */
18 | var capacity = 200
19 |
20 | fun add(particle: Particle) {
21 | particles.addLast(particle)
22 | internalCleanUp()
23 | }
24 |
25 | fun remove(particle: Particle): Boolean {
26 | val returnValue = particles.remove(particle)
27 | internalCleanUp()
28 | return returnValue
29 | }
30 |
31 | fun clear() {
32 | particles.clear()
33 | }
34 |
35 | /**
36 | * Remember to call `nextTick()` after rendering each frame.
37 | */
38 | fun nextTick() {
39 | internalCleanUp()
40 | val recyclable = ArrayList()
41 | for (particle in particles) {
42 | particle.nextTick()
43 | if (particle.recyclable) {
44 | recyclable.add(particle)
45 | }
46 | }
47 | for (particle in recyclable) {
48 | particles.remove(particle)
49 | }
50 | }
51 |
52 | private fun internalCleanUp() {
53 | if (capacity >= 0) {
54 | while (particles.size > capacity) {
55 | particles.removeFirst()
56 | }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/util/MathUtils.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview.util
2 |
3 | import android.graphics.PointF
4 |
5 | class MathUtils {
6 | companion object {
7 | fun getPointOnCircle(center: PointF, radius: Float, deg: Float): PointF {
8 | val rad = deg2rad(deg.toDouble())
9 | val xDelta = (radius * Math.cos(rad)).toFloat()
10 | val yDelta = -(radius * Math.sin(rad)).toFloat()
11 | return PointF(center.x + xDelta, center.y + yDelta)
12 | }
13 |
14 | fun getPointOnCircle(x: Float, y: Float, radius: Float, deg: Float): PointF {
15 | return getPointOnCircle(PointF(x, y), radius, deg)
16 | }
17 |
18 | fun deg2rad(deg: Double): Double {
19 | return deg / 360.0 * 2 * Math.PI
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/sumimakito/rhythmview/wave/WavePoint.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview.wave
2 |
3 | import kotlin.math.abs
4 | import kotlin.math.max
5 | import kotlin.math.min
6 |
7 | /**
8 | * Defines a point on the wave, which possesses its own resistance (velocity) towards value changes.
9 | *
10 | * Parameter `velocity` is calculated by dividing 1 by the frames for the motion tween.
11 | * e.g. If `velocity == 0.1f`, it takes 10 frames for the animation to complete.
12 | */
13 | class WavePoint(private val initialValue: Float, private val velocity: Float, private val minValue: Float, private val maxValue: Float) {
14 | /**
15 | * The value used for displaying or computing the wave path.
16 | *
17 | * Read-only for external accesses.
18 | */
19 | var displayValue: Float = initialValue
20 | private set
21 |
22 | /**
23 | * Final value for the wave point
24 | */
25 | private var targetValue: Float = initialValue
26 | private val delta: Float = abs((maxValue - minValue) * velocity)
27 |
28 | init {
29 | displayValue = initialValue
30 | }
31 |
32 | /**
33 | * Call `changeTo(newValue)` to set a final value for the wave point.
34 | */
35 | fun changeTo(newValue: Float) {
36 | targetValue = min(maxValue, max(minValue, newValue))
37 | }
38 |
39 | /**
40 | * Remember to call `nextTick()` after rendering each frame.
41 | */
42 | fun nextTick() {
43 | if (targetValue == displayValue) return;
44 | if (targetValue > displayValue) {
45 | displayValue = min(targetValue, displayValue + delta)
46 | } else {
47 | displayValue = max(targetValue, displayValue - delta)
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/library/src/test/java/com/github/sumimakito/rhythmview/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.sumimakito.rhythmview
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * @see [Testing documentation](http://d.android.com/tools/testing)
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, (2 + 2).toLong())
15 | }
16 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':demo', ':library'
2 |
--------------------------------------------------------------------------------