├── .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 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 41 | 42 | 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 | 93 | 94 | 95 | 96 | 97 | 98 | 100 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # V-VisualizerView -音频随音谱率动跳动动画效果 2 | * support androidX and Kotlin 3 | 4 | [![License](https://img.shields.io/badge/License%20-Apache%202-337ab7.svg)](https://www.apache.org/licenses/LICENSE-2.0) 5 | [![Arsenal](https://img.shields.io/badge/Arsenal%20-%20VisualizerView-4cae4c.svg)](https://android-arsenal.com/details/1/6001) 6 | [![JCenter](https://api.bintray.com/packages/vension/vensionCenter/V-VisualizerView/images/download.svg)](https://bintray.com/vension/vensionCenter/V-VisualizerView/_latestVersion) 7 | [![MinSdk](https://img.shields.io/badge/%20MinSdk%20-%2016%2B%20-f0ad4e.svg)](https://android-arsenal.com/api?level=16) 8 | [![Author](https://img.shields.io/badge/Author-Vension-orange.svg?style=flat-square)](https://img.shields.io/badge/Author-Vension-orange.svg?style=flat-square) 9 | 10 | ## 效果预览 11 |

12 | 13 |

14 | 15 | ## Download[![Download](https://api.bintray.com/packages/vension/vensionCenter/V-VisualizerView/images/download.svg)](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 | } --------------------------------------------------------------------------------