├── .gitignore
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── encodings.xml
├── gradle.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── vension
│ │ └── visualizerview
│ │ └── demo
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-web.png
│ ├── java
│ │ └── com
│ │ │ └── vension
│ │ │ └── visualizerview
│ │ │ └── demo
│ │ │ ├── JavaActivity.java
│ │ │ └── KotlinActivity.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── background_color.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_pause_circle_outline_black_24dp.xml
│ │ ├── ic_play_circle_outline_black_24dp.xml
│ │ └── seek_bar_dian_selector.xml
│ │ ├── layout
│ │ ├── activity_java.xml
│ │ └── activity_kotlin.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── raw
│ │ ├── videodemo.mp3
│ │ └── z8806c.mp3
│ │ └── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── vension
│ └── visualizerview
│ └── demo
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── preview
├── GIF.gif
└── 录屏_20190705_130233.mp4
├── settings.gradle
├── visualizerview_java
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── vension
│ │ └── visualizerview_java
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── vension
│ │ │ └── visualizerview_java
│ │ │ ├── VisualizerView.java
│ │ │ └── VisualizerView2.java
│ └── res
│ │ └── values
│ │ ├── attrs.xml
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── vension
│ └── visualizerview_java
│ └── ExampleUnitTest.java
└── visualizerview_kotlin
├── .gitignore
├── bintray.gradle
├── build.gradle
├── proguard-rules.pro
└── src
├── androidTest
└── java
│ └── com
│ └── vension
│ └── visualizerview
│ └── kotlin
│ └── ExampleInstrumentedTest.java
├── main
├── AndroidManifest.xml
├── java
│ └── com
│ │ └── vension
│ │ └── visualizerview
│ │ └── kotlin
│ │ ├── VisualizerView.kt
│ │ └── VisualizerView2.kt
└── res
│ └── values
│ ├── attrs.xml
│ └── strings.xml
└── test
└── java
└── com
└── vension
└── visualizerview
└── kotlin
└── ExampleUnitTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/misc.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 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # V-VisualizerView -音频随音谱率动跳动动画效果
2 | * support androidX and Kotlin
3 |
4 | [](https://www.apache.org/licenses/LICENSE-2.0)
5 | [](https://android-arsenal.com/details/1/6001)
6 | [](https://bintray.com/vension/vensionCenter/V-VisualizerView/_latestVersion)
7 | [](https://android-arsenal.com/api?level=16)
8 | [](https://img.shields.io/badge/Author-Vension-orange.svg?style=flat-square)
9 |
10 | ## 效果预览
11 |
12 |
13 |
14 |
15 | ## Download[](https://bintray.com/vension/vensionCenter/V-VisualizerView/_latestVersion)
16 | ``` gradle
17 | implementation 'kv.vension:visualizerview_kotlin:_latestVersion'
18 | ```
19 |
20 | ## Usage
21 |
22 | * **具体使用查看demo示例**
23 |
24 | ## attrs
25 |
26 | | Attribute 属性 | Description 描述 |
27 | |:--- |:---|
28 | | kv_hasShadow | 是否有倒影,默认false |
29 | | kv_shadowNum | 倒影音频块数,默认5 |
30 | | kv_shadowColor | 倒影颜色,默认Color.GRAY |
31 | | kv_visualColor | 音频块颜色,默认Color.RED |
32 | | kv_isGradient | 音频块颜色是否渐变,默认false|
33 | | kv_colorStart | 渐变开始颜色,默认"#A47586" |
34 | | kv_colorCenter | 渐变中间颜色,默认"#C36084" |
35 | | kv_colorEnd | 渐变结束颜色,默认"#F14380" |
36 |
37 | ## update
38 | * **V_1.0.0**: <初始化版本>
39 | * **V_1.0.1**: <降低minSDK到16+>
40 |
41 | ## License
42 | ```
43 | Copyright 2019, Vension
44 |
45 | Licensed under the Apache License, Version 2.0 (the "License");
46 | you may not use this file except in compliance with the License.
47 | You may obtain a copy of the License at
48 |
49 | http://www.apache.org/licenses/LICENSE-2.0
50 |
51 | Unless required by applicable law or agreed to in writing, software
52 | distributed under the License is distributed on an "AS IS" BASIS,
53 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
54 | See the License for the specific language governing permissions and
55 | limitations under the License.
56 | ```
57 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/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 29
9 | defaultConfig {
10 | applicationId "com.vension.visualizerview.demo"
11 | minSdkVersion 16
12 | targetSdkVersion 29
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-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: 'libs', include: ['*.jar'])
27 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
28 | implementation 'androidx.appcompat:appcompat:1.0.2'
29 | implementation 'androidx.core:core-ktx:1.0.2'
30 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
31 | testImplementation 'junit:junit:4.12'
32 | androidTestImplementation 'androidx.test:runner:1.2.0'
33 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
34 | implementation project(path: ':visualizerview_kotlin')
35 | implementation project(path: ':visualizerview_java')
36 | }
37 |
--------------------------------------------------------------------------------
/app/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 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/vension/visualizerview/demo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview.demo
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.vension.visualizerview.demo", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/app/src/main/java/com/vension/visualizerview/demo/JavaActivity.java:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview.demo;
2 |
3 | import android.Manifest;
4 | import android.content.pm.PackageManager;
5 | import android.media.AudioManager;
6 | import android.media.MediaPlayer;
7 | import android.media.audiofx.Equalizer;
8 | import android.media.audiofx.Visualizer;
9 | import android.os.Bundle;
10 | import android.view.Gravity;
11 | import android.view.View;
12 | import android.view.ViewGroup;
13 | import android.widget.*;
14 | import androidx.appcompat.app.AppCompatActivity;
15 | import androidx.core.app.ActivityCompat;
16 | import androidx.core.content.ContextCompat;
17 | import com.vension.visualizerview_java.VisualizerView;
18 | import com.vension.visualizerview_java.VisualizerView2;
19 |
20 | /**
21 | * ========================================================
22 | * 作 者:Vension
23 | * 日 期:2019/7/5 16:03
24 | * 更 新:2019/7/5 16:03
25 | * 描 述:
26 | * ========================================================
27 | */
28 |
29 | public class JavaActivity extends AppCompatActivity {
30 |
31 | private MediaPlayer mMediaPlayer;//音频
32 | private Visualizer mVisualizer;//频谱器
33 | private Equalizer mEqualizer; //均衡器
34 |
35 | private LinearLayout layoutEqualize;//均衡器布局
36 | private VisualizerView mBaseVisualizerView;
37 | private VisualizerView2 mVisualizerView;
38 | private RadioGroup mRadioGroup;
39 | private ImageView ivPlay;
40 |
41 | @Override
42 | protected void onCreate(Bundle savedInstanceState) {
43 | super.onCreate(savedInstanceState);
44 | setVolumeControlStream(AudioManager.STREAM_MUSIC);
45 | setContentView(R.layout.activity_java);
46 |
47 | initView();
48 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
49 | ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, 1);
50 | } else {
51 | init();
52 | }
53 | }
54 |
55 | private void initView() {
56 | mBaseVisualizerView = findViewById(R.id.visualizerView1);
57 | mVisualizerView = findViewById(R.id.visualizerView2);
58 | layoutEqualize = findViewById(R.id.layout_Equalize);
59 | mRadioGroup = findViewById(R.id.mRadioGroup);
60 | ivPlay = findViewById(R.id.iv_play);
61 | mRadioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
62 | @Override
63 | public void onCheckedChanged(RadioGroup group, int checkedId) {
64 | switch (checkedId){
65 | case R.id.rbt_1://无倒影
66 | if (mBaseVisualizerView.getVisibility() == View.GONE){
67 | mBaseVisualizerView.setVisibility(View.VISIBLE);
68 | }
69 | if (mVisualizerView.getVisibility() == View.VISIBLE){
70 | mVisualizerView.setVisibility(View.GONE);
71 | }
72 | mBaseVisualizerView.setHasShadow(false);
73 | //设置允许波形表示,并且捕获它
74 | mBaseVisualizerView.setVisualizer(mVisualizer);//先设置可视化工具
75 | mVisualizer.setEnabled(true);//开启可视化工具
76 | break;
77 | case R.id.rbt_2://有倒影
78 | if (mBaseVisualizerView.getVisibility() == View.GONE){
79 | mBaseVisualizerView.setVisibility(View.VISIBLE);
80 | }
81 | if (mVisualizerView.getVisibility() == View.VISIBLE){
82 | mVisualizerView.setVisibility(View.GONE);
83 | }
84 | mBaseVisualizerView.setHasShadow(true);
85 | //设置允许波形表示,并且捕获它
86 | mBaseVisualizerView.setVisualizer(mVisualizer);//先设置可视化工具
87 | mVisualizer.setEnabled(true);//开启可视化工具
88 | break;
89 | case R.id.rbt_3://颜色渐变
90 | if (mBaseVisualizerView.getVisibility() == View.GONE){
91 | mBaseVisualizerView.setVisibility(View.VISIBLE);
92 | }
93 | if (mVisualizerView.getVisibility() == View.VISIBLE){
94 | mVisualizerView.setVisibility(View.GONE);
95 | }
96 | mBaseVisualizerView.setGradient(true);
97 | mBaseVisualizerView.setHasShadow(true);
98 | //设置允许波形表示,并且捕获它
99 | mBaseVisualizerView.setVisualizer(mVisualizer);//先设置可视化工具
100 | mVisualizer.setEnabled(true);//开启可视化工具
101 | break;
102 | case R.id.rbt_4://颜色分块
103 | if (mVisualizerView.getVisibility() == View.GONE){
104 | mVisualizerView.setVisibility(View.VISIBLE);
105 | }
106 | if (mBaseVisualizerView.getVisibility() == View.VISIBLE){
107 | mBaseVisualizerView.setVisibility(View.GONE);
108 | }
109 | if(mVisualizer != null){
110 | mVisualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() {
111 | @Override
112 | public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
113 |
114 | }
115 |
116 | @Override
117 | public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
118 | mVisualizerView.updateVisualizer(fft);
119 | }
120 | }, Visualizer.getMaxCaptureRate() / 2, true, true);
121 | }
122 | break;
123 | }
124 | }
125 | });
126 | ivPlay.setOnClickListener(new View.OnClickListener() {
127 | @Override
128 | public void onClick(View v) {
129 | if(mMediaPlayer != null){
130 | if (mMediaPlayer.isPlaying()) {
131 | ivPlay.setImageResource(R.drawable.ic_play_circle_outline_black_24dp);
132 |
133 | mMediaPlayer.pause();
134 | } else {
135 | ivPlay.setImageResource(R.drawable.ic_pause_circle_outline_black_24dp);
136 | mMediaPlayer.start();
137 | }
138 | }
139 | }
140 | });
141 | }
142 |
143 | private void init() {
144 | initMediaPlayer();
145 | setVisualizerOnUi();
146 | setupEqualizeFxAndUi();
147 | if(mMediaPlayer != null){
148 | mMediaPlayer.start();
149 | }
150 | }
151 |
152 | private void initMediaPlayer() {
153 | mMediaPlayer = MediaPlayer.create(this, R.raw.videodemo);
154 | mMediaPlayer.setLooping(true);
155 | mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
156 |
157 | @Override
158 | public void onCompletion(MediaPlayer mp) {
159 | // mVisualizer.setEnabled(false);
160 | }
161 | });
162 | }
163 |
164 | /**
165 | * 生成一个VisualizerView对象,使音频频谱的波段能够反映到 VisualizerView上
166 | */
167 | private void setVisualizerOnUi() {
168 | if(mMediaPlayer != null){
169 | //实例化Visualizer,参数SessionId可以通过MediaPlayer的对象获得
170 | mVisualizer = new Visualizer(mMediaPlayer.getAudioSessionId());
171 | //采样 - 参数内必须是2的位数 - 如64,128,256,512,1024
172 | mVisualizer.setCaptureSize(Visualizer.getCaptureSizeRange()[1]);
173 | //设置允许波形表示,并且捕获它
174 | mBaseVisualizerView.setVisualizer(mVisualizer);//先设置可视化工具
175 | mVisualizer.setEnabled(true);//开启可视化工具
176 | }
177 | }
178 |
179 |
180 | /**
181 | * 通过mMediaPlayer返回的AudioSessionId创建一个优先级为0均衡器对象 并且通过频谱生成相应的UI和对应的事件
182 | */
183 | private void setupEqualizeFxAndUi() {
184 | if(mMediaPlayer != null){
185 | mEqualizer = new Equalizer(0, mMediaPlayer.getAudioSessionId());
186 | mEqualizer.setEnabled(true);// 启用均衡器
187 | // 通过均衡器得到其支持的频谱引擎
188 | short bands = mEqualizer.getNumberOfBands();
189 |
190 | // getBandLevelRange 是一个数组,返回一组频谱等级数组,
191 | // 第一个下标为最低的限度范围
192 | // 第二个下标为最大的上限,依次取出
193 | final short minEqualizer = mEqualizer.getBandLevelRange()[0];
194 | final short maxEqualizer = mEqualizer.getBandLevelRange()[1];
195 |
196 | for (short i = 0; i < bands; i++) {
197 | final short band = i;
198 |
199 | TextView freqTextView = new TextView(this);
200 | freqTextView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
201 | freqTextView.setGravity(Gravity.CENTER_HORIZONTAL);
202 | // 取出中心频率
203 | freqTextView.setText((mEqualizer.getCenterFreq(band) / 1000) + "HZ");
204 | layoutEqualize.addView(freqTextView);
205 |
206 | LinearLayout row = new LinearLayout(this);
207 | row.setOrientation(LinearLayout.HORIZONTAL);
208 |
209 | TextView minDbTextView = new TextView(this);
210 | minDbTextView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
211 | minDbTextView.setText((minEqualizer / 100) + " dB");
212 |
213 | TextView maxDbTextView = new TextView(this);
214 | maxDbTextView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
215 | maxDbTextView.setText((maxEqualizer / 100) + " dB");
216 |
217 | LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
218 |
219 | layoutParams.weight = 1;
220 |
221 | SeekBar seekbar = new SeekBar(this);
222 | seekbar.setLayoutParams(layoutParams);
223 | seekbar.setMax(maxEqualizer - minEqualizer);
224 | seekbar.setProgress(mEqualizer.getBandLevel(band));
225 |
226 | seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
227 |
228 | @Override
229 | public void onStopTrackingTouch(SeekBar seekBar) {
230 | }
231 |
232 | @Override
233 | public void onStartTrackingTouch(SeekBar seekBar) {
234 | }
235 |
236 | @Override
237 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
238 | // TODO Auto-generated method stub
239 | mEqualizer.setBandLevel(band, (short) (progress + minEqualizer));
240 | }
241 | });
242 | row.addView(minDbTextView);
243 | row.addView(seekbar);
244 | row.addView(maxDbTextView);
245 | layoutEqualize.addView(row);
246 | }
247 | }
248 | }
249 |
250 |
251 |
252 | @Override
253 | protected void onDestroy() {
254 | super.onDestroy();
255 | if (mMediaPlayer != null) {
256 | mMediaPlayer.release();
257 | mMediaPlayer = null;
258 | }
259 | if (mEqualizer != null) {
260 | mEqualizer.release();
261 | mEqualizer = null;
262 | }
263 | if (mVisualizer != null) {
264 | mVisualizer.release();
265 | mVisualizer = null;
266 | }
267 | }
268 |
269 |
270 | @Override
271 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
272 | if (requestCode == 1) {
273 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
274 | init();
275 | } else {
276 | // Permission Denied
277 | Toast.makeText(JavaActivity.this, "请打开录音权限", Toast.LENGTH_SHORT).show();
278 | }
279 | return;
280 | }
281 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
282 | }
283 |
284 | }
285 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vension/visualizerview/demo/KotlinActivity.kt:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview.demo
2 |
3 | import android.Manifest
4 | import android.content.pm.PackageManager
5 | import android.media.AudioManager
6 | import android.media.MediaPlayer
7 | import android.media.audiofx.Equalizer
8 | import android.media.audiofx.Visualizer
9 | import android.os.Bundle
10 | import android.view.Gravity
11 | import android.view.View
12 | import android.view.ViewGroup
13 | import android.widget.LinearLayout
14 | import android.widget.SeekBar
15 | import android.widget.TextView
16 | import android.widget.Toast
17 | import androidx.appcompat.app.AppCompatActivity
18 | import androidx.core.app.ActivityCompat
19 | import androidx.core.content.ContextCompat
20 | import kotlinx.android.synthetic.main.activity_kotlin.*
21 |
22 | /**
23 | * ========================================================
24 | * 作 者:Vension
25 | * 日 期:2019/7/5 16:03
26 | * 更 新:2019/7/5 16:03
27 | * 描 述:
28 | * ========================================================
29 | */
30 |
31 | class KotlinActivity : AppCompatActivity() {
32 |
33 | private var mMediaPlayer: MediaPlayer? = null//音频
34 | private var mVisualizer: Visualizer? = null//频谱器
35 | private var mEqualizer: Equalizer? = null //均衡器
36 |
37 |
38 | override fun onCreate(savedInstanceState: Bundle?) {
39 | super.onCreate(savedInstanceState)
40 | volumeControlStream = AudioManager.STREAM_MUSIC
41 | setContentView(R.layout.activity_kotlin)
42 |
43 | initView()
44 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
45 | ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1)
46 | } else {
47 | init()
48 | }
49 | }
50 |
51 | private fun initView() {
52 | mRadioGroup.setOnCheckedChangeListener { _, checkedId ->
53 | when (checkedId) {
54 | R.id.rbt_1 -> {//无倒影
55 | if (mVisualizerView.visibility == View.GONE) {
56 | mVisualizerView.visibility = View.VISIBLE
57 | }
58 | if (mVisualizerView2.visibility == View.VISIBLE) {
59 | mVisualizerView2.visibility = View.GONE
60 | }
61 | mVisualizerView.setHasShadow(false)
62 | //设置允许波形表示,并且捕获它
63 | mVisualizerView.setVisualizer(mVisualizer)//先设置可视化工具
64 | mVisualizer?.enabled = true//开启可视化工具
65 | }
66 | R.id.rbt_2 -> {//有倒影
67 | if (mVisualizerView.visibility == View.GONE) {
68 | mVisualizerView.visibility = View.VISIBLE
69 | }
70 | if (mVisualizerView2.visibility == View.VISIBLE) {
71 | mVisualizerView2.visibility = View.GONE
72 | }
73 | mVisualizerView.setHasShadow(true)
74 | //设置允许波形表示,并且捕获它
75 | mVisualizerView.setVisualizer(mVisualizer)//先设置可视化工具
76 | mVisualizer?.enabled = true//开启可视化工具
77 | }
78 | R.id.rbt_3 -> {//颜色渐变
79 | if (mVisualizerView.visibility == View.GONE) {
80 | mVisualizerView.visibility = View.VISIBLE
81 | }
82 | if (mVisualizerView2.visibility == View.VISIBLE) {
83 | mVisualizerView2.visibility = View.GONE
84 | }
85 | mVisualizerView.setGradient(true)
86 | mVisualizerView.setHasShadow(true)
87 | //设置允许波形表示,并且捕获它
88 | mVisualizerView.setVisualizer(mVisualizer)//先设置可视化工具
89 | mVisualizer?.enabled = true//开启可视化工具
90 | }
91 | R.id.rbt_4//颜色分块
92 | -> {
93 | if (mVisualizerView2!!.visibility == View.GONE) {
94 | mVisualizerView2!!.visibility = View.VISIBLE
95 | }
96 | if (mVisualizerView!!.visibility == View.VISIBLE) {
97 | mVisualizerView!!.visibility = View.GONE
98 | }
99 | mVisualizer?.let {
100 | it.setDataCaptureListener(object : Visualizer.OnDataCaptureListener {
101 | override fun onWaveFormDataCapture(visualizer: Visualizer, waveform: ByteArray, samplingRate: Int) {
102 |
103 | }
104 |
105 | override fun onFftDataCapture(visualizer: Visualizer, fft: ByteArray, samplingRate: Int) {
106 | mVisualizerView2.updateVisualizer(fft)
107 | }
108 | }, Visualizer.getMaxCaptureRate() / 2, true, true)
109 | }
110 | }
111 | }
112 | }
113 | iv_play.setOnClickListener {
114 | mMediaPlayer?.let {
115 | if (mMediaPlayer!!.isPlaying) {
116 | iv_play.setImageResource(R.drawable.ic_play_circle_outline_black_24dp)
117 | it.pause()
118 | } else {
119 | iv_play.setImageResource(R.drawable.ic_pause_circle_outline_black_24dp)
120 | it.start()
121 | }
122 | }
123 | }
124 | }
125 |
126 | private fun init() {
127 | initMediaPlayer()
128 | setVisualizerOnUi()
129 | setupEqualizeFxAndUi()
130 | mMediaPlayer!!.start()
131 | }
132 |
133 | private fun initMediaPlayer() {
134 | mMediaPlayer = MediaPlayer.create(this, R.raw.videodemo)
135 | mMediaPlayer?.isLooping = true
136 | mMediaPlayer?.setOnCompletionListener {
137 | // mVisualizer.setEnabled(false);
138 | }
139 | }
140 |
141 | /**
142 | * 生成一个VisualizerView对象,使音频频谱的波段能够反映到 VisualizerView上
143 | */
144 | private fun setVisualizerOnUi() {
145 | if (mMediaPlayer != null) {
146 | //实例化Visualizer,参数SessionId可以通过MediaPlayer的对象获得
147 | mVisualizer = Visualizer(mMediaPlayer!!.audioSessionId)
148 | //采样 - 参数内必须是2的位数 - 如64,128,256,512,1024
149 | mVisualizer?.captureSize = Visualizer.getCaptureSizeRange()[1]
150 | //设置允许波形表示,并且捕获它
151 | mVisualizerView.setVisualizer(mVisualizer)//先设置可视化工具
152 | mVisualizer?.enabled = true//开启可视化工具
153 | }
154 | }
155 |
156 |
157 | /**
158 | * 通过mMediaPlayer返回的AudioSessionId创建一个优先级为0均衡器对象 并且通过频谱生成相应的UI和对应的事件
159 | */
160 | private fun setupEqualizeFxAndUi() {
161 | if (mMediaPlayer != null) {
162 | mEqualizer = Equalizer(0, mMediaPlayer!!.audioSessionId)
163 | mEqualizer?.enabled = true// 启用均衡器
164 | // 通过均衡器得到其支持的频谱引擎
165 | val bands = mEqualizer?.numberOfBands
166 |
167 | // getBandLevelRange 是一个数组,返回一组频谱等级数组,
168 | // 第一个下标为最低的限度范围
169 | // 第二个下标为最大的上限,依次取出
170 | val minEqualizer = mEqualizer!!.bandLevelRange[0]
171 | val maxEqualizer = mEqualizer!!.bandLevelRange[1]
172 | for (i in 0 until bands!!.toShort()) {
173 |
174 | val freqTextView = TextView(this)
175 | freqTextView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
176 | freqTextView.gravity = Gravity.CENTER_HORIZONTAL
177 | // 取出中心频率
178 | freqTextView.text = (mEqualizer!!.getCenterFreq(i.toShort()) / 1000).toString() + "HZ"
179 | layout_Equalize.addView(freqTextView)
180 |
181 | val row = LinearLayout(this)
182 | row.orientation = LinearLayout.HORIZONTAL
183 |
184 | val minDbTextView = TextView(this)
185 | minDbTextView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
186 | minDbTextView.text = (minEqualizer / 100).toString() + " dB"
187 |
188 | val maxDbTextView = TextView(this)
189 | maxDbTextView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
190 | maxDbTextView.text = (maxEqualizer / 100).toString() + " dB"
191 |
192 | val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
193 | layoutParams.weight = 1f
194 |
195 | val seekbar = SeekBar(this)
196 | seekbar.layoutParams = layoutParams
197 | seekbar.max = maxEqualizer - minEqualizer
198 | seekbar.progress = mEqualizer!!.getBandLevel(i.toShort()).toInt()
199 |
200 | seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
201 |
202 | override fun onStopTrackingTouch(seekBar: SeekBar) {}
203 |
204 | override fun onStartTrackingTouch(seekBar: SeekBar) {}
205 |
206 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
207 | // TODO Auto-generated method stub
208 | mEqualizer?.setBandLevel(i.toShort(), (progress + minEqualizer).toShort())
209 | }
210 | })
211 | row.addView(minDbTextView)
212 | row.addView(seekbar)
213 | row.addView(maxDbTextView)
214 | layout_Equalize.addView(row)
215 | }
216 | }
217 | }
218 |
219 |
220 | override fun onDestroy() {
221 | super.onDestroy()
222 | mMediaPlayer!!.release()
223 | mEqualizer!!.release()
224 | mVisualizer!!.release()
225 | }
226 |
227 |
228 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
229 | if (requestCode == 1) {
230 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
231 | init()
232 | } else {
233 | // Permission Denied
234 | Toast.makeText(this@KotlinActivity, "请打开录音权限", Toast.LENGTH_SHORT).show()
235 | }
236 | return
237 | }
238 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
239 | }
240 |
241 | }
242 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pause_circle_outline_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_play_circle_outline_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/seek_bar_dian_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_java.xml:
--------------------------------------------------------------------------------
1 |
8 |
14 |
28 |
36 |
42 |
48 |
54 |
60 |
61 |
67 |
72 |
73 |
80 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_kotlin.xml:
--------------------------------------------------------------------------------
1 |
8 |
14 |
28 |
36 |
42 |
48 |
54 |
60 |
61 |
67 |
72 |
73 |
80 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/raw/videodemo.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/raw/videodemo.mp3
--------------------------------------------------------------------------------
/app/src/main/res/raw/z8806c.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/app/src/main/res/raw/z8806c.mp3
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #071E1C
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | V-VisualizerView
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/vension/visualizerview/demo/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview.demo
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 |
--------------------------------------------------------------------------------
/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.3.40'
5 | repositories {
6 | google()
7 | jcenter()
8 |
9 | }
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:3.4.2'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | //自动上传至Bintray平台插件 --》 https://github.com/dcendents/android-maven-gradle-plugin
16 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
17 | //自动化maven打包插件 --》 https://github.com/bintray/gradle-bintray-plugin
18 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
19 | }
20 | }
21 |
22 | allprojects {
23 | repositories {
24 | google()
25 | jcenter()
26 |
27 | }
28 | }
29 |
30 | task clean(type: Delete) {
31 | delete rootProject.buildDir
32 | }
33 |
--------------------------------------------------------------------------------
/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 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/preview/GIF.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/preview/GIF.gif
--------------------------------------------------------------------------------
/preview/录屏_20190705_130233.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vension/V-VisualizerView/5dc9d45cce717d6de0e2e6b027b52d184c4d16eb/preview/录屏_20190705_130233.mp4
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':visualizerview_kotlin', ':visualizerview_java'
2 |
--------------------------------------------------------------------------------
/visualizerview_java/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/visualizerview_java/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 29
5 |
6 |
7 | defaultConfig {
8 | minSdkVersion 16
9 | targetSdkVersion 29
10 | versionCode 1
11 | versionName "1.0"
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 |
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 |
24 | }
25 |
26 | dependencies {
27 | implementation fileTree(dir: 'libs', include: ['*.jar'])
28 |
29 | implementation 'androidx.appcompat:appcompat:1.0.2'
30 | testImplementation 'junit:junit:4.12'
31 | androidTestImplementation 'androidx.test:runner:1.2.0'
32 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
33 | }
34 |
--------------------------------------------------------------------------------
/visualizerview_java/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 |
--------------------------------------------------------------------------------
/visualizerview_java/src/androidTest/java/com/vension/visualizerview_java/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview_java;
2 |
3 | import android.content.Context;
4 | import androidx.test.InstrumentationRegistry;
5 | import androidx.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.vension.visualizerview_java.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/visualizerview_java/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/visualizerview_java/src/main/java/com/vension/visualizerview_java/VisualizerView.java:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview_java;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.*;
6 | import android.graphics.Paint.Cap;
7 | import android.graphics.Paint.Join;
8 | import android.media.audiofx.Visualizer;
9 | import android.util.AttributeSet;
10 | import android.view.View;
11 |
12 | public class VisualizerView extends View implements Visualizer.OnDataCaptureListener{
13 |
14 | private static final int DN_W = 480;//view宽度与单个音频块占比 - 正常480 需微调
15 | private static final int DN_H = 360;//view高度与单个音频块占比
16 | private static final int DN_SL = 15;//单个音频块宽度
17 | private static final int DN_SW = 5;//单个音频块高度
18 | protected final static int MAX_LEVEL = 30;//音量柱·音频块 - 最大个数
19 | protected final static int CYLINDER_NUM = 26;//音量柱 - 最大个数
20 |
21 | private boolean hasShadow = false;//是否有倒影
22 | private int shadowNum = 5;//倒影个数
23 | private int shadowColor = Color.GRAY;//倒影颜色
24 | private int visualColor = Color.RED;//频块颜色
25 | private boolean isGradient = false;//音频块颜色是否渐变
26 | private int colorStart = Color.parseColor("#A47586");
27 | private int colorCenter = Color.parseColor("#C36084");
28 | private int colorEnd = Color.parseColor("#F14380");
29 |
30 | private int hgap = 0;
31 | private int vgap = 0;
32 | private int levelStep = 0;
33 | private float strokeWidth = 0;
34 | private float strokeLength = 0;
35 |
36 |
37 | /**
38 | * It is the visualizer.
39 | */
40 | protected Visualizer mVisualizer = null;//频谱器
41 |
42 | /**
43 | * It is the paint which is used to draw to visual effect.
44 | */
45 | protected Paint mPaint = null;//画笔
46 |
47 | /**
48 | * It is the buffer of fft.
49 | */
50 | protected byte[] mData = new byte[CYLINDER_NUM];//音量柱 数组
51 |
52 | boolean mDataEn = true;
53 |
54 | /**
55 | * It constructs the base visualizer view.
56 | * 构造函数初始化画笔
57 | * @param context It is the context of the view owner.
58 | */
59 | public VisualizerView(Context context) {
60 | this(context, null);
61 |
62 | }
63 |
64 | public VisualizerView(Context context, AttributeSet attrs) {
65 | super(context, attrs);
66 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.VisualizerView, 0, 0);
67 | hasShadow = ta.getBoolean(R.styleable.VisualizerView_kv_hasShadow,false);
68 | shadowNum = ta.getInteger(R.styleable.VisualizerView_kv_shadowNum,5);
69 | shadowColor = ta.getColor(R.styleable.VisualizerView_kv_shadowColor, Color.GRAY);
70 | visualColor = ta.getColor(R.styleable.VisualizerView_kv_visualColor, Color.RED);
71 | isGradient = ta.getBoolean(R.styleable.VisualizerView_kv_isGradient,false);
72 | colorStart = ta.getColor(R.styleable.VisualizerView_kv_colorStart, Color.parseColor("#A47586"));
73 | colorCenter = ta.getColor(R.styleable.VisualizerView_kv_colorCenter, Color.parseColor("#C36084"));
74 | colorEnd = ta.getColor(R.styleable.VisualizerView_kv_colorEnd, Color.parseColor("#F14380"));
75 | ta.recycle();
76 | init();
77 | }
78 |
79 | private void init() {
80 | mPaint = new Paint();//初始化画笔工具
81 | mPaint.setAntiAlias(true);//抗锯齿
82 | mPaint.setColor(0xFFd60d25);//画笔颜色
83 | mPaint.setStrokeJoin(Join.ROUND);//频块圆角
84 | mPaint.setStrokeCap(Cap.ROUND);//频块圆角
85 | }
86 |
87 | //执行 Layout 操作
88 | @Override
89 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
90 | super.onLayout(changed, left, top, right, bottom);
91 |
92 | float w, h, xr, yr;
93 | w = right - left;
94 | h = bottom - top;
95 | xr = w / (float)DN_W;
96 | yr = h / (float)DN_H;
97 |
98 | strokeWidth = DN_SW * yr;
99 | strokeLength = DN_SL * xr;
100 | hgap = (int)((w - strokeLength * CYLINDER_NUM) / (CYLINDER_NUM + 1) );
101 | vgap = (int)(h / (MAX_LEVEL + 2));//频谱块高度
102 |
103 | mPaint.setStrokeWidth(strokeWidth); //设置频谱块宽度
104 | }
105 |
106 |
107 | @Override
108 | public void onDraw(Canvas canvas) {
109 | for (int i = 0; i < CYLINDER_NUM; i ++) {
110 | drawCylinder(canvas, strokeWidth / 2 + hgap + i * (hgap + strokeLength), mData[i]);
111 | }
112 |
113 | // int j=-4;
114 | // for (int i = 0; i < CYLINDER_NUM/2-4; i++) { //绘制25个能量柱
115 | //
116 | // drawCylinder(canvas, strokeWidth / 2 + hgap + i * (hgap + strokeLength), mData[i]);
117 | // }
118 | // for(int i =CYLINDER_NUM; i>=CYLINDER_NUM/2-4; i--){
119 | // j++;
120 | // drawCylinder(canvas, strokeWidth / 2 + hgap + (CYLINDER_NUM/2+j-1 )* (hgap + strokeLength), mData[i-1]);
121 | // }
122 | }
123 |
124 |
125 | /**
126 | * 绘制频谱块和倒影
127 | */
128 | protected void drawCylinder(Canvas canvas, float x, byte value) {
129 | float y;
130 | if (value <= 0) value = 1;
131 | for (int i = 0; i < value; i++) {//每个能量柱绘制value个能量块
132 | if(hasShadow){//是否有倒影
133 | y = (getHeight()/2 - i * vgap - vgap);//计算y轴坐标
134 | float y1=(getHeight()/2+i * vgap + vgap);
135 | //绘制音量柱倒影
136 | if (i <= shadowNum && value > 0) {
137 | mPaint.setColor(shadowColor);//画笔颜色
138 | mPaint.setAlpha(100 - (100 / shadowNum * i));//倒影颜色
139 | canvas.drawLine(x, y1, (x + strokeLength), y1, mPaint);//绘制频谱块
140 | }
141 | }else{
142 | y = getHeight() - i * vgap - vgap;
143 | }
144 | //绘制频谱块
145 | mPaint.setColor(visualColor);//画笔颜色
146 | if(isGradient){
147 | LinearGradient backGradient = new LinearGradient(x, y, (x + strokeLength), y, new int[]{colorStart, colorCenter ,colorEnd}, null, Shader.TileMode.CLAMP);
148 | mPaint.setShader(backGradient);
149 | }
150 | canvas.drawLine(x, y, (x + strokeLength), y, mPaint);//绘制频谱块
151 | }
152 | }
153 |
154 | /**
155 | * It sets the visualizer of the view. DO set the viaulizer to null when exit the program.
156 | * @parma visualizer It is the visualizer to set.
157 | */
158 | public void setVisualizer(Visualizer visualizer) {
159 | if (visualizer != null) {
160 | if (!visualizer.getEnabled()) {
161 | visualizer.setCaptureSize(Visualizer.getCaptureSizeRange()[0]);
162 | }
163 | levelStep = 240 / MAX_LEVEL;
164 | visualizer.setDataCaptureListener(this, Visualizer.getMaxCaptureRate() / 2, false, true);
165 |
166 | } else {
167 |
168 | if (mVisualizer != null) {
169 | mVisualizer.setEnabled(false);
170 | mVisualizer.release();
171 | }
172 | }
173 | mVisualizer = visualizer;
174 | }
175 |
176 |
177 | //这个回调应该采集的是快速傅里叶变换有关的数据
178 | @Override
179 | public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
180 | byte[] model = new byte[fft.length / 2 + 1];
181 | if (mDataEn) {
182 | model[0] = (byte) Math.abs(fft[1]);
183 | int j = 1;
184 | for (int i = 2; i < fft.length; ) {
185 | model[j] = (byte) Math.hypot(fft[i], fft[i + 1]);
186 | i += 2;
187 | j++;
188 | }
189 | } else {
190 | for (int i = 0; i < CYLINDER_NUM; i++) {
191 | model[i] = 0;
192 | }
193 | }
194 | for (int i = 0; i < CYLINDER_NUM; i++) {
195 | final byte a = (byte) (Math.abs(model[CYLINDER_NUM - i]) / levelStep);
196 |
197 | final byte b = mData[i];
198 | if (a > b) {
199 | mData[i] = a;
200 | } else {
201 | if (b > 0) {
202 | mData[i]--;
203 | }
204 | }
205 | }
206 | postInvalidate();//刷新界面
207 | }
208 |
209 | //这个回调应该采集的是波形数据
210 | @Override
211 | public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
212 | // Do nothing...
213 | }
214 |
215 | /**
216 | * It enables or disables the data processs.
217 | * @param en If this value is true it enables the data process..
218 | */
219 | public void enableDataProcess(boolean en) {
220 | mDataEn = en;
221 | }
222 |
223 | public boolean isHasShadow() {
224 | return hasShadow;
225 | }
226 |
227 | public void setHasShadow(boolean hasShadow) {
228 | this.hasShadow = hasShadow;
229 | }
230 |
231 | public int getShadowNum() {
232 | return shadowNum;
233 | }
234 |
235 | public void setShadowNum(int shadowNum) {
236 | this.shadowNum = shadowNum;
237 | }
238 |
239 | public int getShadowColor() {
240 | return shadowColor;
241 | }
242 |
243 | public void setShadowColor(int shadowColor) {
244 | this.shadowColor = shadowColor;
245 | }
246 |
247 | public int getVisualColor() {
248 | return visualColor;
249 | }
250 |
251 | public void setVisualColor(int visualColor) {
252 | this.visualColor = visualColor;
253 | }
254 |
255 | public boolean isGradient() {
256 | return isGradient;
257 | }
258 |
259 | public void setGradient(boolean gradient) {
260 | isGradient = gradient;
261 | }
262 |
263 | public int getColorStart() {
264 | return colorStart;
265 | }
266 |
267 | public void setColorStart(int colorStart) {
268 | this.colorStart = colorStart;
269 | }
270 |
271 | public int getColorCenter() {
272 | return colorCenter;
273 | }
274 |
275 | public void setColorCenter(int colorCenter) {
276 | this.colorCenter = colorCenter;
277 | }
278 |
279 | public int getColorEnd() {
280 | return colorEnd;
281 | }
282 |
283 | public void setColorEnd(int colorEnd) {
284 | this.colorEnd = colorEnd;
285 | }
286 |
287 | }
288 |
--------------------------------------------------------------------------------
/visualizerview_java/src/main/java/com/vension/visualizerview_java/VisualizerView2.java:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview_java;
2 |
3 |
4 | import android.annotation.SuppressLint;
5 | import android.content.Context;
6 | import android.content.res.TypedArray;
7 | import android.graphics.*;
8 | import android.util.AttributeSet;
9 | import android.view.View;
10 |
11 |
12 | /**
13 | * 自定义View——随音乐频谱跳动的线条
14 | */
15 | public class VisualizerView2 extends View {
16 | private int mColor;// 主色调
17 | private int mLineWidth;// 频谱线条宽度
18 | private int mSpeceNum;// 空隙个数(不设置自己计算)
19 | private int mSpeceWidth;// 空隙宽度
20 | private int mBaseHeight;// 基础高度
21 | private boolean mLineIsSingleColor; // 线条只有一种颜色
22 | private int mFirstPartColor; // 频谱线条支持多种颜色
23 | private int mSecondPartColor;
24 | private int mThirdPartColor;
25 | private int mFourthPartColor;
26 |
27 | private byte[] mBytes;
28 | private float[] mPoints;
29 | private Rect mRect = new Rect();
30 |
31 | private Paint mPaint = new Paint();
32 | private int mMinPoint;
33 | private int mhalfPoint;
34 |
35 | public VisualizerView2(Context context, AttributeSet attributeSet) {
36 | super(context, attributeSet);
37 | TypedArray t = context.obtainStyledAttributes(attributeSet, R.styleable.VisualizerView2, 0, 0);
38 | mColor = t.getColor(R.styleable.VisualizerView2_lineColor, Color.parseColor("#FFFFFF"));
39 | mSpeceNum = t.getInteger(R.styleable.VisualizerView2_spaceNum, 0);
40 | mSpeceWidth = t.getDimensionPixelSize(R.styleable.VisualizerView2_spaceWidth, 0);
41 | mLineWidth = t.getDimensionPixelSize(R.styleable.VisualizerView2_lineWidth, 5);
42 | mBaseHeight = t.getDimensionPixelSize(R.styleable.VisualizerView2_baseHeight, 1);
43 | mLineIsSingleColor = t.getBoolean(R.styleable.VisualizerView2_lineIsSingleColor, true);
44 | mFirstPartColor = t.getColor(R.styleable.VisualizerView2_firstPartColor, Color.parseColor("#FFFFFF"));
45 | mSecondPartColor = t.getColor(R.styleable.VisualizerView2_secondPartColor, Color.parseColor("#FFFFFF"));
46 | mThirdPartColor = t.getColor(R.styleable.VisualizerView2_thirdPartColor, Color.parseColor("#FFFFFF"));
47 | mFourthPartColor = t.getColor(R.styleable.VisualizerView2_fourthPartColor, Color.parseColor("#FFFFFF"));
48 | t.recycle();
49 | init();
50 | }
51 |
52 | private void init() {
53 | mMinPoint = dip2px(getContext(), 2);
54 | mhalfPoint = dip2px(getContext(), 1);
55 | mBytes = null;
56 | mPaint.setStrokeWidth(mLineWidth);
57 | mPaint.setAntiAlias(true);
58 | if (mLineIsSingleColor) {
59 | mPaint.setColor(mColor);
60 | } else {
61 | mPaint.setColor(mColor);
62 | }
63 | }
64 |
65 | public void updateVisualizer(byte[] fft) {
66 | byte[] model = new byte[fft.length / 2 + 1];
67 | model[0] = (byte) Math.abs(fft[0]);
68 | if (mSpeceNum == 0) {
69 | int width = getWidth();
70 | mSpeceNum = (width + mLineWidth) / (mSpeceWidth + mLineWidth);
71 | }
72 | for (int i = 2, j = 1; j < mSpeceNum; ) {
73 | model[j] = (byte) Math.hypot(fft[i], fft[i + 1]);
74 | i += 2;
75 | j++;
76 | }
77 | mBytes = model;
78 | invalidate();
79 | }
80 |
81 | @Override
82 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
83 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
84 | int widthMode = MeasureSpec.getMode(widthMeasureSpec);
85 | int widthSize = MeasureSpec.getSize(widthMeasureSpec);
86 |
87 | int heightMode = MeasureSpec.getMode(heightMeasureSpec);
88 | int heightSize = MeasureSpec.getSize(heightMeasureSpec);
89 |
90 | // 设置wrap_content的默认宽 / 高值
91 | int mWidth = dip2px(getContext(), 200);
92 | int mHeight = dip2px(getContext(), 100);
93 |
94 | if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
95 | setMeasuredDimension(mWidth, mHeight);
96 | } else if (widthMode == MeasureSpec.AT_MOST) {
97 | setMeasuredDimension(mWidth, heightSize);
98 | } else if (heightMode == MeasureSpec.AT_MOST) {
99 | setMeasuredDimension(widthSize, mHeight);
100 | }
101 | }
102 |
103 | @SuppressLint("DrawAllocation")
104 | @Override
105 | protected void onDraw(Canvas canvas) {
106 | super.onDraw(canvas);
107 | if (mBytes == null) {
108 | return;
109 | }
110 | if (mPoints == null || mPoints.length < mBytes.length * 4) {
111 | mPoints = new float[mBytes.length * 4];
112 | }
113 | mRect.set(0, 0, getWidth(), getHeight());
114 | final int baseX = mRect.width() / mSpeceNum;
115 | final int baseLine = mRect.height();
116 |
117 | for (int i = 0; i < mSpeceNum; i++) {
118 | if (mBytes[i] < 0) {
119 | mBytes[i] = 127;
120 | }
121 | final int xi = baseX * i + baseX;
122 | final int xi2 = baseX * i;
123 | if (i != mSpeceNum - 1) {
124 | mPoints[i * 4] = xi;
125 | mPoints[i * 4 + 2] = xi;
126 | } else {
127 | mPoints[i * 4] = xi2;
128 | mPoints[i * 4 + 2] = xi2;
129 | }
130 | float offset = mBytes[i] * 3f + mBaseHeight;
131 | if (offset <= mMinPoint) {
132 | mPoints[i * 4 + 1] = baseLine + mhalfPoint;
133 | mPoints[i * 4 + 3] = baseLine - mhalfPoint;
134 | } else {
135 | mPoints[i * 4 + 1] = baseLine + offset;
136 | mPoints[i * 4 + 3] = baseLine - offset;
137 | }
138 | if (!mLineIsSingleColor) {
139 | mPaint.setShader(new LinearGradient(mPoints[i * 4], mPoints[i * 4 + 1], mPoints[i * 4 + 2], mPoints[i * 4 + 3],
140 | new int[]{mFirstPartColor, mFirstPartColor,
141 | mSecondPartColor, mSecondPartColor,
142 | mThirdPartColor, mThirdPartColor,
143 | mFourthPartColor, mFourthPartColor},
144 | new float[]{0f, 0.65f, 0.64f, 0.75f, 0.74f, 0.85f, 0.84f, 1f}, Shader.TileMode.CLAMP));
145 |
146 | canvas.drawLine(mPoints[i * 4], mPoints[i * 4 + 1], mPoints[i * 4 + 2], mPoints[i * 4 + 3], mPaint);
147 | }
148 | }
149 | if (mLineIsSingleColor) {
150 | canvas.drawLines(mPoints, mPaint);
151 | }
152 | }
153 |
154 |
155 | /**
156 | * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
157 | */
158 | private int dip2px(Context context, float dpValue) {
159 | final float scale = context.getResources().getDisplayMetrics().density;
160 | return (int) (dpValue * scale + 0.5f);
161 | }
162 |
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/visualizerview_java/src/main/res/values/attrs.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 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/visualizerview_java/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | VisualizerView_Java
3 |
4 |
--------------------------------------------------------------------------------
/visualizerview_java/src/test/java/com/vension/visualizerview_java/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview_java;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/visualizerview_kotlin/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/visualizerview_kotlin/bintray.gradle:
--------------------------------------------------------------------------------
1 | // 添加配置
2 | apply plugin: 'com.github.dcendents.android-maven'
3 | apply plugin: 'com.jfrog.bintray'
4 |
5 |
6 | /**
7 | * 定义两个链接,下面会用到。
8 | * @param siteUrl 项目主页
9 | * @param gitUrl 项目的Git仓库的url
10 | * @param libName 发布到JCenter上的项目名字
11 | * @param libDesc 项目描述
12 | */
13 | def siteUrl = 'https://github.com/Vension/V-VisualizerView'
14 | def gitUrl = 'https://github.com/Vension/V-VisualizerView.git'
15 | def libName = "V-VisualizerView"
16 | def libDesc = ":point_right:Android realizes the animation effect of the rate beat of audio with sound spectrum.(安卓实现音频随音谱率动跳动动画效果)"
17 |
18 | /**
19 | * @param group 发布到组织名称名字,必须填写
20 | * @param version 版本号,下次更新是只需要更改版本号即可
21 | * (kv.vension.xxx)这样写是不好的,项目名会拼上去
22 | * 我手欠然后最后就是这样了,大家引以为戒 implementation 'kv.vension.xxx:xxx:1.0.0'
23 | * 配置后上传至JCenter后的编译路径是这样的: implementation 'kv.vension.xxx:1.0.0'
24 | */
25 | group = "kv.vension"// 唯一包名,比如implementation 'kv.vension.xxx:1.0.0'中的me.vension就是这里配置的。
26 | version = "1.0.1"//项目引用的版本号,比如implementation 'kv.vension.xxx:1.0.0'中的1.0.0就是这里配置的。
27 |
28 |
29 | // 生成jar包的task,不需要修改。
30 | task sourcesJar(type: Jar) {
31 | from android.sourceSets.main.java.srcDirs
32 | classifier = 'sources'
33 | }
34 |
35 | // 生成jarDoc的task,不需要修改。
36 | task javadoc(type: Javadoc) {
37 | source = android.sourceSets.main.java.srcDirs
38 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
39 | // destinationDir = file("../javadoc/")
40 | failOnError false // 忽略注释语法错误,如果用jdk1.8你的注释写的不规范就编译不过。
41 | }
42 |
43 |
44 | // 文档打包成jar/生成javaDoc的jar,不需要修改。
45 | task javadocJar(type: Jar, dependsOn: javadoc) {
46 | classifier = 'javadoc'
47 | from javadoc.destinationDir
48 | }
49 |
50 | //拷贝javadoc文件 不需要修改。
51 | task copyDoc(type: Copy) {
52 | from "${buildDir}/docs/"
53 | into "docs"
54 | }
55 |
56 | //上传到JCenter所需要的源码文件 不需要修改。
57 | artifacts {
58 | archives javadocJar
59 | archives sourcesJar
60 | }
61 |
62 | // 配置maven库,生成POM.xml文件
63 | install {
64 | repositories.mavenInstaller {
65 | // 生成pom.xml和参数
66 | // This generates POM.xml with proper parameters
67 | pom {
68 | project {
69 | packaging 'aar'
70 | // 项目描述,复制我的话,这里需要修改。
71 | name libName// 可选,项目名称。
72 | description libDesc// 可选,项目描述。
73 | url siteUrl // 项目主页,这里是引用上面定义好。
74 |
75 | // 软件开源协议,现在一般都是Apache License2.0吧,复制我的,这里不需要修改。
76 | licenses {
77 | license {
78 | name 'The Apache Software License, Version 2.0'
79 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
80 | }
81 | }
82 |
83 | //填写开发者基本信息,复制我的,这里需要修改。
84 | developers {
85 | developer {
86 | id 'vension' // 开发者的id。
87 | name 'KevinHu' // 开发者名字。
88 | email '2506856664@qq.com' // 开发者邮箱。
89 | }
90 | }
91 |
92 | // SCM,复制我的,这里不需要修改。
93 | scm {
94 | connection gitUrl // Git仓库地址。
95 | developerConnection gitUrl // Git仓库地址。
96 | url siteUrl // 项目主页。
97 | }
98 |
99 | }
100 | }
101 | }
102 | }
103 |
104 |
105 |
106 | // =========================== 上传到JCenter ===================================================
107 |
108 | // 这里是读取Bintray相关的信息,我们上传项目到github上的时候会把gradle文件传上去,所以不要把帐号密码的信息直接写在这里,写在local.properties中,这里动态读取。
109 | Properties properties = new Properties()
110 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
111 | bintray {
112 | user = properties.getProperty("bintray.user") //Bintray的用户名:读取 local.properties 文件里面的 bintray.user
113 | key = properties.getProperty("bintray.apikey") //Bintray的ApiKey:读取 local.properties 文件里面的 bintray.apikey
114 | configurations = ['archives']
115 | pkg {
116 | //注意:这里的repo值必须要和你创建Maven仓库的时候的名字一样
117 | repo = "vensionCenter" //Repository名字 需要自己在bintray网站上先添加
118 | //发布到JCenter上的项目名字
119 | name = libName
120 | userOrg = 'vension'//Bintray的组织id
121 | //项目描述
122 | desc = libDesc
123 | websiteUrl = siteUrl
124 | vcsUrl = gitUrl
125 | licenses = ["Apache-2.0"]
126 | publish = true// 是否是公开项目。
127 | }
128 | }
129 |
130 | javadoc {
131 | options{
132 | //如果你的项目里面有中文注释的话,必须将格式设置为UTF-8,不然会出现乱码
133 | encoding "UTF-8"
134 | charSet 'UTF-8'
135 | author true
136 | version true
137 | links "http://docs.oracle.com/javase/7/docs/api"
138 | }
139 | }
140 |
141 |
142 | /**
143 | * 执行上传命令
144 | * 1、打开Android Studio底部工具栏的Terminal,输入命令:
145 | * windows:gradlew install
146 | * mac:./gradlew install
147 | * BUILD SUCCESSFUL说明执行成功。
148 | * 2、上传到Bintray
149 | * windows:gradlew bintrayUpload
150 | * mac:./gradlew bintrayUpload
151 | * BUILD SUCCESSFUL说明上传成功。
152 | */
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/visualizerview_kotlin/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | android {
5 | compileSdkVersion 29
6 |
7 | defaultConfig {
8 | minSdkVersion 16
9 | targetSdkVersion 29
10 | versionCode 1
11 | versionName "1.0"
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 |
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 |
24 | }
25 |
26 | dependencies {
27 | implementation fileTree(dir: 'libs', include: ['*.jar'])
28 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
29 | implementation 'androidx.appcompat:appcompat:1.0.2'
30 | testImplementation 'junit:junit:4.12'
31 | androidTestImplementation 'androidx.test:runner:1.2.0'
32 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
33 | }
34 |
35 | apply from: 'bintray.gradle'
--------------------------------------------------------------------------------
/visualizerview_kotlin/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 |
--------------------------------------------------------------------------------
/visualizerview_kotlin/src/androidTest/java/com/vension/visualizerview/kotlin/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview.kotlin;
2 |
3 | import android.content.Context;
4 | import androidx.test.InstrumentationRegistry;
5 | import androidx.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.vension.visualizerview.kotlin.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/visualizerview_kotlin/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/visualizerview_kotlin/src/main/java/com/vension/visualizerview/kotlin/VisualizerView.kt:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview.kotlin
2 |
3 | import android.content.Context
4 | import android.graphics.*
5 | import android.graphics.Paint.Cap
6 | import android.graphics.Paint.Join
7 | import android.media.audiofx.Visualizer
8 | import android.util.AttributeSet
9 | import android.view.View
10 | import kotlin.math.abs
11 | import kotlin.math.hypot
12 |
13 | /**
14 | * ========================================================
15 | * 作 者:Vension
16 | * 日 期:2019/7/5 16:03
17 | * 更 新:2019/7/5 16:03
18 | * 描 述:音频随音谱率动跳动动画效果
19 | * ========================================================
20 | */
21 |
22 | class VisualizerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : View(context, attrs),
23 | Visualizer.OnDataCaptureListener {
24 |
25 | companion object {
26 | private const val DN_W = 480//view宽度与单个音频块占比 - 正常480 需微调
27 | private const val DN_H = 360//view高度与单个音频块占比
28 | private const val DN_SL = 15//单个音频块宽度
29 | private const val DN_SW = 5//单个音频块高度
30 | private const val MAX_LEVEL = 30//音量柱·音频块 - 最大个数
31 | private const val CYLINDER_NUM = 26//音量柱 - 最大个数
32 | }
33 |
34 | private var isHasShadow = false//是否有倒影
35 | private var shadowNum = 5//倒影个数
36 | private var shadowColor = Color.GRAY//倒影颜色
37 | private var visualColor = Color.RED//频块颜色
38 | private var isGradient = false//音频块颜色是否渐变
39 | private var colorStart = Color.parseColor("#A47586")
40 | private var colorCenter = Color.parseColor("#C36084")
41 | private var colorEnd = Color.parseColor("#F14380")
42 |
43 | private var hgap = 0
44 | private var vgap = 0
45 | private var levelStep = 0
46 | private var strokeWidth = 0f
47 | private var strokeLength = 0f
48 |
49 |
50 | /**
51 | * It is the visualizer.
52 | */
53 | private var mVisualizer: Visualizer? = null//频谱器
54 |
55 | /**
56 | * It is the paint which is used to draw to visual effect.
57 | */
58 | private var mPaint: Paint? = null//画笔
59 |
60 | /**
61 | * It is the buffer of fft.
62 | */
63 | private var mData = ByteArray(CYLINDER_NUM)//音量柱 数组
64 |
65 | private var mDataEn = true
66 |
67 | init {
68 | val ta = context.obtainStyledAttributes(attrs, R.styleable.VisualizerView, 0, 0)
69 | isHasShadow = ta.getBoolean(R.styleable.VisualizerView_kv_hasShadow, false)
70 | shadowNum = ta.getInteger(R.styleable.VisualizerView_kv_shadowNum, 5)
71 | shadowColor = ta.getColor(R.styleable.VisualizerView_kv_shadowColor, Color.GRAY)
72 | visualColor = ta.getColor(R.styleable.VisualizerView_kv_visualColor, Color.RED)
73 | isGradient = ta.getBoolean(R.styleable.VisualizerView_kv_isGradient, false)
74 | colorStart = ta.getColor(R.styleable.VisualizerView_kv_colorStart, Color.parseColor("#A47586"))
75 | colorCenter = ta.getColor(R.styleable.VisualizerView_kv_colorCenter, Color.parseColor("#C36084"))
76 | colorEnd = ta.getColor(R.styleable.VisualizerView_kv_colorEnd, Color.parseColor("#F14380"))
77 | ta.recycle()
78 | init()
79 | }
80 |
81 | private fun init() {
82 | mPaint = Paint()//初始化画笔工具
83 | mPaint!!.isAntiAlias = true//抗锯齿
84 | mPaint!!.color = -0x29f2db//画笔颜色
85 | mPaint!!.strokeJoin = Join.ROUND//频块圆角
86 | mPaint!!.strokeCap = Cap.ROUND//频块圆角
87 | }
88 |
89 | //执行 Layout 操作
90 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
91 | super.onLayout(changed, left, top, right, bottom)
92 |
93 | var w: Float = (right - left).toFloat()
94 | val h: Float = (bottom - top).toFloat()
95 | val xr: Float = w / DN_W.toFloat()
96 | val yr: Float = h / DN_H.toFloat()
97 |
98 | strokeWidth = DN_SW * yr
99 | strokeLength = DN_SL * xr
100 | hgap = ((w - strokeLength * CYLINDER_NUM) / (CYLINDER_NUM + 1)).toInt()
101 | vgap = (h / (MAX_LEVEL + 2)).toInt()//频谱块高度
102 | mPaint!!.strokeWidth = strokeWidth //设置频谱块宽度
103 | }
104 |
105 |
106 | public override fun onDraw(canvas: Canvas) {
107 | for (i in 0 until CYLINDER_NUM) {
108 | drawCylinder(canvas, strokeWidth / 2 + hgap.toFloat() + i * (hgap + strokeLength), mData[i])
109 | }
110 |
111 | // int j=-4;
112 | // for (int i = 0; i < CYLINDER_NUM/2-4; i++) { //绘制25个能量柱
113 | //
114 | // drawCylinder(canvas, strokeWidth / 2 + hgap + i * (hgap + strokeLength), mData[i]);
115 | // }
116 | // for(int i =CYLINDER_NUM; i>=CYLINDER_NUM/2-4; i--){
117 | // j++;
118 | // drawCylinder(canvas, strokeWidth / 2 + hgap + (CYLINDER_NUM/2+j-1 )* (hgap + strokeLength), mData[i-1]);
119 | // }
120 | }
121 |
122 |
123 | /**
124 | * 绘制频谱块和倒影
125 | */
126 | private fun drawCylinder(canvas: Canvas, x: Float, value: Byte) {
127 | var value = value
128 | var y: Float
129 | if (value <= 0) value = 1
130 | for (i in 0 until value) {//每个能量柱绘制value个能量块
131 | if (isHasShadow) {//是否有倒影
132 | y = (height / 2 - i * vgap - vgap).toFloat()//计算y轴坐标
133 | val y1 = (height / 2 + i * vgap + vgap).toFloat()
134 | //绘制音量柱倒影
135 | if (i <= shadowNum && value > 0) {
136 | mPaint!!.color = shadowColor//画笔颜色
137 | mPaint!!.alpha = 100 - 100 / shadowNum * i//倒影颜色
138 | canvas.drawLine(x, y1, x + strokeLength, y1, mPaint!!)//绘制频谱块
139 | }
140 | } else {
141 | y = (height - i * vgap - vgap).toFloat()
142 | }
143 | //绘制频谱块
144 | mPaint!!.color = visualColor//画笔颜色
145 | if (isGradient) {
146 | val backGradient = LinearGradient(x, y, x + strokeLength, y, intArrayOf(colorStart, colorCenter, colorEnd), null, Shader.TileMode.CLAMP)
147 | mPaint?.shader = backGradient
148 | }
149 | canvas.drawLine(x, y, x + strokeLength, y, mPaint!!)//绘制频谱块
150 | }
151 | }
152 |
153 | /**
154 | * It sets the visualizer of the view. DO set the viaulizer to null when exit the program.
155 | * @parma visualizer It is the visualizer to set.
156 | */
157 | fun setVisualizer(visualizer: Visualizer?) {
158 | if (visualizer != null) {
159 | if (!visualizer.enabled) {
160 | visualizer.captureSize = Visualizer.getCaptureSizeRange()[0]
161 | }
162 | levelStep = 240 / MAX_LEVEL
163 | visualizer.setDataCaptureListener(this, Visualizer.getMaxCaptureRate() / 2, false, true)
164 | } else {
165 | if (mVisualizer != null) {
166 | mVisualizer?.enabled = false
167 | mVisualizer?.release()
168 | }
169 | }
170 | mVisualizer = visualizer
171 | }
172 |
173 |
174 | //这个回调应该采集的是快速傅里叶变换有关的数据
175 | override fun onFftDataCapture(visualizer: Visualizer, fft: ByteArray, samplingRate: Int) {
176 | val model = ByteArray(fft.size / 2 + 1)
177 | if (mDataEn) {
178 | model[0] = abs(fft[1].toInt()).toByte()
179 | var j = 1
180 | var i = 2
181 | while (i < fft.size) {
182 | model[j] = hypot(fft[i].toDouble(), fft[i + 1].toDouble()).toByte()
183 | i += 2
184 | j++
185 | }
186 | } else {
187 | for (i in 0 until CYLINDER_NUM) {
188 | model[i] = 0
189 | }
190 | }
191 | for (i in 0 until CYLINDER_NUM) {
192 | val a = (abs(model[CYLINDER_NUM - i].toInt()) / levelStep).toByte()
193 |
194 | val b = mData[i]
195 | if (a > b) {
196 | mData[i] = a
197 | } else {
198 | if (b > 0) {
199 | mData[i]--
200 | }
201 | }
202 | }
203 | postInvalidate()//刷新界面
204 | }
205 |
206 | //这个回调应该采集的是波形数据
207 | override fun onWaveFormDataCapture(visualizer: Visualizer, waveform: ByteArray, samplingRate: Int) {
208 | // Do nothing...
209 | }
210 |
211 | /**
212 | * It enables or disables the data processs.
213 | * @param en If this value is true it enables the data process..
214 | */
215 | fun enableDataProcess(en: Boolean) {
216 | mDataEn = en
217 | }
218 |
219 | fun isHasShadow(): Boolean {
220 | return isHasShadow
221 | }
222 |
223 | fun setHasShadow(hasShadow: Boolean) {
224 | this.isHasShadow = hasShadow
225 | invalidate()
226 | }
227 |
228 | fun getShadowNum(): Int {
229 | return shadowNum
230 | }
231 |
232 | fun setShadowNum(shadowNum: Int) {
233 | this.shadowNum = shadowNum
234 | invalidate()
235 | }
236 |
237 | fun getShadowColor(): Int {
238 | return shadowColor
239 | }
240 |
241 | fun setShadowColor(shadowColor: Int) {
242 | this.shadowColor = shadowColor
243 | invalidate()
244 | }
245 |
246 | fun getVisualColor(): Int {
247 | return visualColor
248 | }
249 |
250 | fun setVisualColor(visualColor: Int) {
251 | this.visualColor = visualColor
252 | invalidate()
253 | }
254 |
255 | fun isGradient(): Boolean {
256 | return isGradient
257 | }
258 |
259 | fun setGradient(gradient: Boolean) {
260 | isGradient = gradient
261 | invalidate()
262 | }
263 |
264 | fun getColorStart(): Int {
265 | return colorStart
266 | }
267 |
268 | fun setColorStart(colorStart: Int) {
269 | this.colorStart = colorStart
270 | invalidate()
271 | }
272 |
273 | fun getColorCenter(): Int {
274 | return colorCenter
275 | }
276 |
277 | fun setColorCenter(colorCenter: Int) {
278 | this.colorCenter = colorCenter
279 | invalidate()
280 | }
281 |
282 | fun getColorEnd(): Int {
283 | return colorEnd
284 | }
285 |
286 | fun setColorEnd(colorEnd: Int) {
287 | this.colorEnd = colorEnd
288 | invalidate()
289 | }
290 |
291 | }
--------------------------------------------------------------------------------
/visualizerview_kotlin/src/main/java/com/vension/visualizerview/kotlin/VisualizerView2.kt:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview.kotlin
2 |
3 |
4 | import android.annotation.SuppressLint
5 | import android.content.Context
6 | import android.graphics.*
7 | import android.util.AttributeSet
8 | import android.view.View
9 | import kotlin.math.abs
10 | import kotlin.math.hypot
11 |
12 |
13 | /**
14 | * 自定义View——随音乐频谱跳动的线条
15 | */
16 | class VisualizerView2(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
17 | private val mColor: Int// 主色调
18 | private val mLineWidth: Int// 频谱线条宽度
19 | private var mSpeceNum: Int = 0// 空隙个数(不设置自己计算)
20 | private val mSpeceWidth: Int// 空隙宽度
21 | private val mBaseHeight: Int// 基础高度
22 | private val mLineIsSingleColor: Boolean // 线条只有一种颜色
23 | private val mFirstPartColor: Int // 频谱线条支持多种颜色
24 | private val mSecondPartColor: Int
25 | private val mThirdPartColor: Int
26 | private val mFourthPartColor: Int
27 |
28 | private var mBytes: ByteArray? = null
29 | private var mPoints: FloatArray? = null
30 | private val mRect = Rect()
31 |
32 | private val mPaint = Paint()
33 | private var mMinPoint: Int = 0
34 | private var mhalfPoint: Int = 0
35 |
36 | init {
37 | val t = context.obtainStyledAttributes(attributeSet, R.styleable.VisualizerView2, 0, 0)
38 | mColor = t.getColor(R.styleable.VisualizerView2_lineColor, Color.parseColor("#FFFFFF"))
39 | mSpeceNum = t.getInteger(R.styleable.VisualizerView2_spaceNum, 0)
40 | mSpeceWidth = t.getDimensionPixelSize(R.styleable.VisualizerView2_spaceWidth, 0)
41 | mLineWidth = t.getDimensionPixelSize(R.styleable.VisualizerView2_lineWidth, 5)
42 | mBaseHeight = t.getDimensionPixelSize(R.styleable.VisualizerView2_baseHeight, 1)
43 | mLineIsSingleColor = t.getBoolean(R.styleable.VisualizerView2_lineIsSingleColor, true)
44 | mFirstPartColor = t.getColor(R.styleable.VisualizerView2_firstPartColor, Color.parseColor("#FFFFFF"))
45 | mSecondPartColor = t.getColor(R.styleable.VisualizerView2_secondPartColor, Color.parseColor("#FFFFFF"))
46 | mThirdPartColor = t.getColor(R.styleable.VisualizerView2_thirdPartColor, Color.parseColor("#FFFFFF"))
47 | mFourthPartColor = t.getColor(R.styleable.VisualizerView2_fourthPartColor, Color.parseColor("#FFFFFF"))
48 | t.recycle()
49 | init()
50 | }
51 |
52 | private fun init() {
53 | mMinPoint = dip2px(context, 2f)
54 | mhalfPoint = dip2px(context, 1f)
55 | mBytes = null
56 | mPaint.strokeWidth = mLineWidth.toFloat()
57 | mPaint.isAntiAlias = true
58 | if (mLineIsSingleColor) {
59 | mPaint.color = mColor
60 | } else {
61 | mPaint.color = mColor
62 | }
63 | }
64 |
65 | fun updateVisualizer(fft: ByteArray) {
66 | val model = ByteArray(fft.size / 2 + 1)
67 | model[0] = abs(fft[0].toInt()).toByte()
68 | if (mSpeceNum == 0) {
69 | val width = width
70 | mSpeceNum = (width + mLineWidth) / (mSpeceWidth + mLineWidth)
71 | }
72 | var i = 2
73 | var j = 1
74 | while (j < mSpeceNum) {
75 | model[j] = hypot(fft[i].toDouble(), fft[i + 1].toDouble()).toByte()
76 | i += 2
77 | j++
78 | }
79 | mBytes = model
80 | invalidate()
81 | }
82 |
83 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
84 | super.onMeasure(widthMeasureSpec, heightMeasureSpec)
85 | val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
86 | val widthSize = View.MeasureSpec.getSize(widthMeasureSpec)
87 |
88 | val heightMode = View.MeasureSpec.getMode(heightMeasureSpec)
89 | val heightSize = View.MeasureSpec.getSize(heightMeasureSpec)
90 |
91 | // 设置wrap_content的默认宽 / 高值
92 | val mWidth = dip2px(context, 200f)
93 | val mHeight = dip2px(context, 100f)
94 |
95 | if (widthMode == View.MeasureSpec.AT_MOST && heightMode == View.MeasureSpec.AT_MOST) {
96 | setMeasuredDimension(mWidth, mHeight)
97 | } else if (widthMode == View.MeasureSpec.AT_MOST) {
98 | setMeasuredDimension(mWidth, heightSize)
99 | } else if (heightMode == View.MeasureSpec.AT_MOST) {
100 | setMeasuredDimension(widthSize, mHeight)
101 | }
102 | }
103 |
104 | @SuppressLint("DrawAllocation")
105 | override fun onDraw(canvas: Canvas) {
106 | super.onDraw(canvas)
107 | if (mBytes == null) {
108 | return
109 | }
110 | if (mPoints == null || mPoints!!.size < mBytes!!.size * 4) {
111 | mPoints = FloatArray(mBytes!!.size * 4)
112 | }
113 | mRect.set(0, 0, width, height)
114 | val baseX = mRect.width() / mSpeceNum
115 | val baseLine = mRect.height()
116 | for (i in 0 until mSpeceNum){
117 | if (mBytes!![i] < 0) {
118 | mBytes!![i] = 127
119 | }
120 | val xi = baseX * i + baseX
121 | val xi2 = baseX * i
122 | if (i != mSpeceNum - 1) {
123 | mPoints!![i * 4] = xi.toFloat()
124 | mPoints!![i * 4 + 2] = xi.toFloat()
125 | } else {
126 | mPoints!![i * 4] = xi2.toFloat()
127 | mPoints!![i * 4 + 2] = xi2.toFloat()
128 | }
129 | val offset = mBytes!![i] * 3f + mBaseHeight
130 | if (offset <= mMinPoint) {
131 | mPoints!![i * 4 + 1] = (baseLine + mhalfPoint).toFloat()
132 | mPoints!![i * 4 + 3] = (baseLine - mhalfPoint).toFloat()
133 | } else {
134 | mPoints!![i * 4 + 1] = baseLine + offset
135 | mPoints!![i * 4 + 3] = baseLine - offset
136 | }
137 | if (!mLineIsSingleColor) {
138 | mPaint.shader = LinearGradient(
139 | mPoints!![i * 4], mPoints!![i * 4 + 1], mPoints!![i * 4 + 2], mPoints!![i * 4 + 3],
140 | intArrayOf(
141 | mFirstPartColor,
142 | mFirstPartColor,
143 | mSecondPartColor,
144 | mSecondPartColor,
145 | mThirdPartColor,
146 | mThirdPartColor,
147 | mFourthPartColor,
148 | mFourthPartColor
149 | ),
150 | floatArrayOf(0f, 0.65f, 0.64f, 0.75f, 0.74f, 0.85f, 0.84f, 1f), Shader.TileMode.CLAMP
151 | )
152 |
153 | canvas.drawLine(
154 | mPoints!![i * 4],
155 | mPoints!![i * 4 + 1],
156 | mPoints!![i * 4 + 2],
157 | mPoints!![i * 4 + 3],
158 | mPaint
159 | )
160 | }
161 | }
162 | if (mLineIsSingleColor) {
163 | canvas.drawLines(mPoints!!, mPaint)
164 | }
165 | }
166 |
167 |
168 | /**
169 | * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
170 | */
171 | private fun dip2px(context: Context, dpValue: Float): Int {
172 | val scale = context.resources.displayMetrics.density
173 | return (dpValue * scale + 0.5f).toInt()
174 | }
175 |
176 |
177 | }
178 |
--------------------------------------------------------------------------------
/visualizerview_kotlin/src/main/res/values/attrs.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 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/visualizerview_kotlin/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Visualizerview
3 |
4 |
--------------------------------------------------------------------------------
/visualizerview_kotlin/src/test/java/com/vension/visualizerview/kotlin/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.vension.visualizerview.kotlin;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------