├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── build │ └── outputs │ │ └── apk │ │ └── debug │ │ └── app-debug.apk ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── an │ │ └── customview │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── an │ │ │ ├── customview │ │ │ ├── ActivityCircleProcessBar.java │ │ │ ├── ActivityCompassView.java │ │ │ ├── ActivityDFCompassView.java │ │ │ ├── ActivityGeneralSpectrumView.java │ │ │ ├── ActivityLevelStreamView.java │ │ │ ├── ActivityProgressBar.java │ │ │ ├── ActivityRadarView.java │ │ │ ├── ActivityScaleBar.java │ │ │ ├── ActivitySpectrumView.java │ │ │ ├── ActivityWaterfullView.java │ │ │ ├── BaseActivity.java │ │ │ ├── MainActivity.java │ │ │ └── readan.txt │ │ │ └── view │ │ │ ├── CircleProcessBar.java │ │ │ ├── CompassView.java │ │ │ ├── DFCompassView.java │ │ │ ├── GeneralSpectrumView.java │ │ │ ├── GradientColorDialog.java │ │ │ ├── GradientColorView.java │ │ │ ├── LevelStreamView.java │ │ │ ├── OnDrawFinishedListener.java │ │ │ ├── ProgressBar.java │ │ │ ├── RadarView.java │ │ │ ├── ScaleBar.java │ │ │ ├── ShowMode.java │ │ │ ├── SpectrumView.java │ │ │ ├── Utils.java │ │ │ ├── WaterfallCanvas.java │ │ │ └── WaterfallView.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── car.png │ │ ├── df.png │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_circle_process_bar.xml │ │ ├── activity_compass_view.xml │ │ ├── activity_df.xml │ │ ├── activity_general_spectrum_view.xml │ │ ├── activity_level_stream_view.xml │ │ ├── activity_main.xml │ │ ├── activity_progress_bar.xml │ │ ├── activity_radar_view.xml │ │ ├── activity_scale_bar.xml │ │ ├── activity_spectrum_view.xml │ │ ├── activity_waterfull_view.xml │ │ └── layout_gradient_color_dialog.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── an_48px.png │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── an │ └── customview │ └── ExampleUnitTest.java ├── build.gradle ├── doc ├── 安卓自定义瀑布图控件.md ├── 安卓自定义电平流控件.md └── 安卓自定义频谱图控件.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 安卓自定义控件,有电平流、频谱图、瀑布图、刻度条、指南针、进度条,待添加.... 3 | 4 | ## LevelStremView ## 5 | 6 | 电平流控件,样式如下: 7 | 8 | ![](https://i.imgur.com/UnHQy0I.png) 9 | 10 | ![](https://i.imgur.com/TsYjyyB.gif) 11 | 12 | ## SpectrumView WaterfallView ## 13 | 14 | 频谱图控件,样式如下: 15 | 16 | ![](https://i.imgur.com/tthngvy.png) 17 | 18 | 瀑布图控件,样色如下: 19 | 20 | ![](https://i.imgur.com/mi26pyj.png) 21 | 22 | 通用频谱图控件(频谱图与瀑布图的组合) 23 | 24 | ![](https://i.imgur.com/jySDEka.png) 25 | 26 | ## ScaleBar ## 27 | 28 | 刻度条控件,样式如下: 29 | 30 | ![](https://i.imgur.com/hkmSzSD.png) 31 | 32 | ## CompassView ## 33 | 34 | 指南针控件,样式如下: 35 | 36 | ![](https://i.imgur.com/toa8tdV.png) 37 | 38 | ## ProgressBar ## 39 | 40 | 进度条控件,样式如下: 41 | 42 | ![](https://i.imgur.com/wSYbgz2.png) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' // application library 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion "29.0.1" 6 | defaultConfig { 7 | applicationId "com.an.customview" 8 | // ERROR: Library projects cannot set applicationId. applicationId is set to 'com.an.customview' in default config. 9 | minSdkVersion 15 10 | targetSdkVersion 29 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | debug { 17 | minifyEnabled true 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | release { 21 | // signingConfig signingConfigs.release 22 | minifyEnabled true 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | implementation 'androidx.appcompat:appcompat:1.0.2' 31 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 32 | testImplementation 'junit:junit:4.12' 33 | androidTestImplementation 'androidx.test:runner:1.2.0' 34 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 35 | } 36 | -------------------------------------------------------------------------------- /app/build/outputs/apk/debug/app-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/build/outputs/apk/debug/app-debug.apk -------------------------------------------------------------------------------- /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 | 23 | #指定压缩级别 24 | -optimizationpasses 5 25 | 26 | #不跳过非公共的库的类成员 27 | -dontskipnonpubliclibraryclassmembers 28 | 29 | #混淆时采用的算法 30 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 31 | 32 | #把混淆类中的方法名也混淆了 33 | -useuniqueclassmembernames 34 | 35 | #优化时允许访问并修改有修饰符的类和类的成员 36 | -allowaccessmodification 37 | 38 | #将文件来源重命名为“SourceFile”字符串 39 | -renamesourcefileattribute SourceFile 40 | #保留行号 41 | -keepattributes SourceFile,LineNumberTable 42 | #保持泛型 43 | -keepattributes Signature 44 | 45 | #保持所有实现 Serializable 接口的类成员 46 | -keepclassmembers class * implements java.io.Serializable { 47 | static final long serialVersionUID; 48 | private static final java.io.ObjectStreamField[] serialPersistentFields; 49 | private void writeObject(java.io.ObjectOutputStream); 50 | private void readObject(java.io.ObjectInputStream); 51 | java.lang.Object writeReplace(); 52 | java.lang.Object readResolve(); 53 | } 54 | 55 | #Fragment不需要在AndroidManifest.xml中注册,需要额外保护下 56 | -keep public class * extends android.support.v4.app.Fragment 57 | -keep public class * extends android.app.Fragment 58 | 59 | # 保持测试相关的代码 60 | -dontnote junit.framework.** 61 | -dontnote junit.runner.** 62 | -dontwarn android.test.** 63 | -dontwarn android.support.test.** 64 | -dontwarn org.junit.** 65 | -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":1,"versionName":"1.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] -------------------------------------------------------------------------------- /app/src/androidTest/java/com/an/customview/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.InstrumentationRegistry; 6 | import androidx.test.runner.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("com.an.customview", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/ActivityCircleProcessBar.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.an.view.CircleProcessBar; 6 | 7 | public class ActivityCircleProcessBar extends BaseActivity { 8 | 9 | private CircleProcessBar cpb1; 10 | private CircleProcessBar cpb2; 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_circle_process_bar); 16 | 17 | initView(); 18 | } 19 | 20 | private void initView() { 21 | cpb1 = (CircleProcessBar) findViewById(R.id.cpb_1); 22 | cpb2 = (CircleProcessBar) findViewById(R.id.cpb_2); 23 | 24 | _runing = true; 25 | 26 | new Thread() { 27 | @Override 28 | public void run() { 29 | super.run(); 30 | int value = 0; 31 | 32 | while (_runing) { 33 | cpb1.setValue(value); 34 | cpb2.setValue(value); 35 | 36 | value++; 37 | 38 | if (value > 100) { 39 | value = 0; 40 | } 41 | 42 | try { 43 | Thread.sleep(500); 44 | } catch (InterruptedException e) { 45 | e.printStackTrace(); 46 | } 47 | } 48 | } 49 | }.start(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/ActivityCompassView.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.an.view.CompassView; 6 | 7 | import java.util.Random; 8 | 9 | public class ActivityCompassView extends BaseActivity { 10 | 11 | private CompassView _compassView1; 12 | private CompassView _compassView2; 13 | private CompassView _compassView3; 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_compass_view); 19 | 20 | initView(); 21 | } 22 | 23 | private void initView() { 24 | _compassView1 = (CompassView) findViewById(R.id.compass_view1); 25 | _compassView2 = (CompassView) findViewById(R.id.compass_view2); 26 | _compassView3 = (CompassView) findViewById(R.id.compass_view3); 27 | 28 | _runing = true; 29 | final Random random = new Random(); 30 | 31 | new Thread() { 32 | @Override 33 | public void run() { 34 | super.run(); 35 | 36 | while (_runing) { 37 | int value = random.nextInt(360); 38 | _compassView1.setAngle(value); 39 | _compassView2.setAngle(value); 40 | _compassView3.setAngle(value); 41 | 42 | try { 43 | Thread.sleep(500); 44 | } catch (InterruptedException e) { 45 | e.printStackTrace(); 46 | } 47 | } 48 | } 49 | }.start(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/ActivityDFCompassView.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.an.view.DFCompassView; 6 | 7 | import java.util.Random; 8 | 9 | public class ActivityDFCompassView extends BaseActivity { 10 | 11 | private final Random rand = new Random(); 12 | private DFCompassView _dfCompassView1; 13 | private DFCompassView _dfCompassView2; 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_df); 19 | 20 | initView(); 21 | } 22 | 23 | private void initView() { 24 | _runing = true; 25 | _dfCompassView1 = (DFCompassView) findViewById(R.id.df_view1); 26 | _dfCompassView2 = (DFCompassView) findViewById(R.id.df_view2); 27 | _dfCompassView2.setNorthMode(DFCompassView.NorthMode.CAR_HEAD); 28 | _dfCompassView2.setViewMode(DFCompassView.ViewMode.COMPASS); 29 | 30 | new Thread() { 31 | @Override 32 | public void run() { 33 | super.run(); 34 | float angle = 0; 35 | 36 | while (_runing) { 37 | float quality = 80 + (rand.nextInt(199 - (-200) + 1) + (-200)) / 10; 38 | float azimuth = 160 + (rand.nextInt(150 - (-150) + 1) + (-150)) / 10; 39 | angle += 2; 40 | if (angle >= 360) { 41 | angle = 0; 42 | } 43 | 44 | _dfCompassView1.setData(azimuth, quality, angle); 45 | _dfCompassView2.setData(azimuth, quality, angle); 46 | 47 | try { 48 | Thread.sleep(1000); 49 | } catch (InterruptedException e) { 50 | e.printStackTrace(); 51 | } 52 | } 53 | } 54 | }.start(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/ActivityGeneralSpectrumView.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | import android.widget.CheckBox; 5 | import android.widget.CompoundButton; 6 | import android.widget.RadioGroup; 7 | 8 | import com.an.view.GeneralSpectrumView; 9 | import com.an.view.ShowMode; 10 | 11 | import java.util.Random; 12 | 13 | public class ActivityGeneralSpectrumView extends BaseActivity implements RadioGroup.OnCheckedChangeListener { 14 | 15 | private GeneralSpectrumView _spectrumWaterfall_1; 16 | private final Random rand = new Random(); 17 | private RadioGroup _radioGroup; 18 | private CheckBox _chMax; 19 | private CheckBox _cbMin; 20 | 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | setContentView(R.layout.activity_general_spectrum_view); 26 | 27 | initView(); 28 | } 29 | 30 | private void initView() { 31 | _spectrumWaterfall_1 = (GeneralSpectrumView) findViewById(R.id.spectrum_waterfall_view); 32 | _radioGroup = (RadioGroup) findViewById(R.id.rg_mode); 33 | _radioGroup.setOnCheckedChangeListener(this); 34 | _chMax = (CheckBox) findViewById(R.id.cb_max_line); 35 | _cbMin = (CheckBox) findViewById(R.id.cb_min_line); 36 | _chMax.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 37 | @Override 38 | public void onCheckedChanged(CompoundButton compoundButton, boolean b) { 39 | _spectrumWaterfall_1.setMaxValueVisible(b); 40 | } 41 | }); 42 | _cbMin.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 43 | @Override 44 | public void onCheckedChanged(CompoundButton compoundButton, boolean b) { 45 | _spectrumWaterfall_1.setMinValueVisible(b); 46 | } 47 | }); 48 | 49 | _runing = true; 50 | 51 | new Thread() { 52 | @Override 53 | public void run() { 54 | super.run(); 55 | 56 | while (_runing) { 57 | _spectrumWaterfall_1.setData(101.7, 20000, getSpectrumData()); 58 | 59 | try { 60 | Thread.sleep(50); 61 | } catch (InterruptedException e) { 62 | e.printStackTrace(); 63 | } 64 | } 65 | } 66 | }.start(); 67 | } 68 | 69 | private float[] getSpectrumData() { 70 | float[] data = new float[801]; 71 | 72 | for (int i = 0; i < 801; i++) { 73 | data[i] = (rand.nextInt(50 - (-150) + 1) + (-150)) / 10; 74 | } 75 | 76 | data[199] = 17 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 77 | data[200] = 27 + (rand.nextInt(10 - (-10) + 1) + (-10)) / 10; 78 | data[201] = 17 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 79 | 80 | data[399] = 27 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 81 | data[400] = 47 + (rand.nextInt(10 - (-10) + 1) + (-10)) / 10; 82 | data[401] = 27 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 83 | 84 | data[599] = 17 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 85 | data[600] = 27 + (rand.nextInt(10 - (-10) + 1) + (-10)) / 10; 86 | data[601] = 17 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 87 | 88 | return data; 89 | } 90 | 91 | @Override 92 | public void onCheckedChanged(RadioGroup radioGroup, int i) { 93 | ShowMode mode = ShowMode.Both; 94 | switch (i) { 95 | case R.id.rbt_both: 96 | mode = ShowMode.Both; 97 | break; 98 | case R.id.rbt_spectrum: 99 | mode = ShowMode.Spectrum; 100 | break; 101 | case R.id.rbt_waterfall: 102 | mode = ShowMode.Waterfall; 103 | break; 104 | } 105 | 106 | _spectrumWaterfall_1.setShowMode(mode); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/ActivityLevelStreamView.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | import android.widget.Button; 6 | 7 | import com.an.view.LevelStreamView; 8 | 9 | import java.util.Random; 10 | 11 | public class ActivityLevelStreamView extends BaseActivity implements View.OnClickListener { 12 | 13 | private LevelStreamView levelStreamView1; 14 | private LevelStreamView levelStreamView3; 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_level_stream_view); 20 | 21 | initView(); 22 | } 23 | 24 | @Override 25 | public void onClick(View view) { 26 | switch (view.getId()) { 27 | case R.id.btn_zoom_in: 28 | levelStreamView1.zoomLevel(2); 29 | levelStreamView3.zoomLevel(2); 30 | break; 31 | case R.id.btn_zoom_out: 32 | levelStreamView1.zoomLevel(-2); 33 | levelStreamView3.zoomLevel(-2); 34 | break; 35 | case R.id.btn_offset_up: 36 | levelStreamView1.offsetLevel(-2); 37 | levelStreamView3.offsetLevel(-2); 38 | break; 39 | case R.id.btn_offset_down: 40 | levelStreamView1.offsetLevel(2); 41 | levelStreamView3.offsetLevel(2); 42 | break; 43 | case R.id.btn_auto: 44 | levelStreamView3.autoView(); 45 | levelStreamView1.autoView(); 46 | break; 47 | case R.id.btn_clear: 48 | levelStreamView1.clear(); 49 | levelStreamView3.clear(); 50 | break; 51 | } 52 | } 53 | 54 | private void initView() { 55 | levelStreamView1 = (LevelStreamView) findViewById(R.id.level_stream_view1); 56 | levelStreamView3 = (LevelStreamView) findViewById(R.id.level_stream_view3); 57 | _runing = true; 58 | 59 | Button btnZoomIn = (Button) findViewById(R.id.btn_zoom_in); 60 | Button btnZoomOut = (Button) findViewById(R.id.btn_zoom_out); 61 | Button btnOffsetUp = (Button) findViewById(R.id.btn_offset_up); 62 | Button btnOffsetDown = (Button) findViewById(R.id.btn_offset_down); 63 | Button btnClear = (Button) findViewById(R.id.btn_clear); 64 | Button btnAuto = (Button) findViewById(R.id.btn_auto); 65 | btnZoomIn.setOnClickListener(this); 66 | btnZoomOut.setOnClickListener(this); 67 | btnOffsetUp.setOnClickListener(this); 68 | btnOffsetDown.setOnClickListener(this); 69 | btnClear.setOnClickListener(this); 70 | btnAuto.setOnClickListener(this); 71 | 72 | new Thread() { 73 | @Override 74 | public void run() { 75 | super.run(); 76 | 77 | Random random = new Random(); 78 | 79 | while (_runing) { 80 | float level = random.nextInt(50); 81 | levelStreamView1.setLevel(level); 82 | levelStreamView3.setLevel(level); 83 | 84 | try { 85 | Thread.sleep(50); 86 | } catch (InterruptedException e) { 87 | e.printStackTrace(); 88 | } 89 | } 90 | } 91 | }.start(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/ActivityProgressBar.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.an.view.ProgressBar; 6 | 7 | public class ActivityProgressBar extends BaseActivity { 8 | 9 | private ProgressBar _progressBar1; 10 | private ProgressBar _progressBar2; 11 | private ProgressBar _progressBar3; 12 | private ProgressBar _progressBar4; 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_progress_bar); 18 | 19 | initView(); 20 | } 21 | 22 | private void initView() { 23 | _progressBar1 = (ProgressBar) findViewById(R.id.progress_bar1); 24 | _progressBar2 = (ProgressBar) findViewById(R.id.progress_bar2); 25 | _progressBar3 = (ProgressBar) findViewById(R.id.progress_bar3); 26 | _progressBar4 = (ProgressBar) findViewById(R.id.progress_bar4); 27 | 28 | _runing = true; 29 | 30 | new Thread() { 31 | @Override 32 | public void run() { 33 | super.run(); 34 | 35 | while (_runing) { 36 | for (int i = 0; i <= 100; i++) { 37 | _progressBar1.setValue(i); 38 | _progressBar2.setValue(i); 39 | _progressBar3.setValue(i); 40 | _progressBar4.setValue(i); 41 | 42 | try { 43 | Thread.sleep(500); 44 | } catch (InterruptedException e) { 45 | e.printStackTrace(); 46 | } 47 | } 48 | } 49 | } 50 | }.start(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/ActivityRadarView.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | 5 | public class ActivityRadarView extends BaseActivity { 6 | 7 | @Override 8 | protected void onCreate(Bundle savedInstanceState) { 9 | super.onCreate(savedInstanceState); 10 | setContentView(R.layout.activity_radar_view); 11 | 12 | initView(); 13 | } 14 | 15 | private void initView() { 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/ActivityScaleBar.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.an.view.ScaleBar; 6 | 7 | import java.util.Random; 8 | 9 | public class ActivityScaleBar extends BaseActivity { 10 | 11 | private ScaleBar _scaleBar1; 12 | private ScaleBar _scaleBar2; 13 | private ScaleBar _scaleBar3; 14 | private ScaleBar _scaleBar4; 15 | 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_scale_bar); 21 | 22 | // 启动线程,设置 Value 23 | initView(); 24 | } 25 | 26 | private void initView() { 27 | _scaleBar1 = (ScaleBar) findViewById(R.id.scale_bar_1); 28 | _scaleBar2 = (ScaleBar) findViewById(R.id.scale_bar_2); 29 | _scaleBar3 = (ScaleBar) findViewById(R.id.scale_bar_3); 30 | _scaleBar4 = (ScaleBar) findViewById(R.id.scale_bar_4); 31 | 32 | _runing = true; 33 | new Thread() { 34 | @Override 35 | public void run() { 36 | super.run(); 37 | 38 | while (_runing) { 39 | Random rand = new Random(); 40 | int value = rand.nextInt(100); 41 | _scaleBar1.setValue(value); 42 | _scaleBar2.setValue(value); 43 | _scaleBar3.setValue(value); 44 | _scaleBar4.setValue(value); 45 | 46 | try { 47 | Thread.sleep(500); 48 | } catch (InterruptedException e) { 49 | e.printStackTrace(); 50 | } 51 | } 52 | } 53 | }.start(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/ActivitySpectrumView.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | import android.widget.Button; 6 | import android.widget.CheckBox; 7 | import android.widget.CompoundButton; 8 | 9 | import com.an.view.SpectrumView; 10 | 11 | import java.util.Random; 12 | 13 | public class ActivitySpectrumView extends BaseActivity implements View.OnClickListener { 14 | 15 | private SpectrumView _spectrumView1; 16 | private SpectrumView _spectrumView2; 17 | private final Random rand = new Random(); 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_spectrum_view); 23 | 24 | initView(); 25 | } 26 | 27 | private void initView() { 28 | _spectrumView1 = (SpectrumView) findViewById(R.id.spectrum_view_1); 29 | _spectrumView2 = (SpectrumView) findViewById(R.id.spectrum_view_2); 30 | _spectrumView2.setMaxValueLineVisible(true); 31 | _spectrumView2.setMinValueLineVisible(true); 32 | 33 | Button btnZoomInY = (Button) findViewById(R.id.btn_zoom_in_sv); 34 | Button btnZoomOutY = (Button) findViewById(R.id.btn_zoom_out_sv); 35 | Button btnOffsetUp = (Button) findViewById(R.id.btn_offset_up_sv); 36 | Button btnOffsetDown = (Button) findViewById(R.id.btn_offset_down_sv); 37 | Button btnClear = (Button) findViewById(R.id.btn_clear_sv); 38 | Button btnAuto = (Button) findViewById(R.id.btn_auto_sv); 39 | CheckBox cbMax = (CheckBox) findViewById(R.id.cb_max); 40 | CheckBox cbMin = (CheckBox) findViewById(R.id.cb_min); 41 | 42 | btnZoomInY.setOnClickListener(this); 43 | btnZoomOutY.setOnClickListener(this); 44 | btnOffsetUp.setOnClickListener(this); 45 | btnOffsetDown.setOnClickListener(this); 46 | btnClear.setOnClickListener(this); 47 | btnAuto.setOnClickListener(this); 48 | 49 | cbMax.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 50 | @Override 51 | public void onCheckedChanged(CompoundButton compoundButton, boolean b) { 52 | _spectrumView2.setMaxValueLineVisible(b); 53 | } 54 | }); 55 | cbMin.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 56 | @Override 57 | public void onCheckedChanged(CompoundButton compoundButton, boolean b) { 58 | _spectrumView2.setMinValueLineVisible(b); 59 | } 60 | }); 61 | 62 | _runing = true; 63 | 64 | new Thread() { 65 | @Override 66 | public void run() { 67 | super.run(); 68 | 69 | while (_runing) { 70 | float[] data = getSpectremData(); 71 | 72 | _spectrumView1.setData(101.7, 2000, data); 73 | _spectrumView2.setData(101.7, 2000, data); 74 | 75 | try { 76 | Thread.sleep(50); 77 | } catch (InterruptedException e) { 78 | e.printStackTrace(); 79 | } 80 | } 81 | } 82 | }.start(); 83 | } 84 | 85 | @Override 86 | public void onClick(View view) { 87 | switch (view.getId()) { 88 | case R.id.btn_zoom_in_sv: 89 | _spectrumView1.zoomY(2); 90 | _spectrumView2.zoomY(2); 91 | break; 92 | case R.id.btn_zoom_out_sv: 93 | _spectrumView1.zoomY(-2); 94 | _spectrumView2.zoomY(-2); 95 | break; 96 | case R.id.btn_offset_up_sv: 97 | _spectrumView1.offsetY(-2); 98 | _spectrumView2.offsetY(-2); 99 | break; 100 | case R.id.btn_offset_down_sv: 101 | _spectrumView1.offsetY(2); 102 | _spectrumView2.offsetY(2); 103 | break; 104 | case R.id.btn_clear_sv: 105 | _spectrumView1.clear(); 106 | _spectrumView2.clear(); 107 | break; 108 | case R.id.btn_auto_sv: 109 | _spectrumView1.autoView(); 110 | _spectrumView2.autoView(); 111 | break; 112 | 113 | } 114 | } 115 | 116 | private float[] getSpectremData() { 117 | float[] data = new float[801]; 118 | 119 | for (int i = 0; i < 801; i++) { 120 | data[i] = (rand.nextInt(50 - (-150) + 1) + (-150)) / 10; 121 | } 122 | 123 | data[399] = 27 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 124 | data[400] = 47 + (rand.nextInt(10 - (-10) + 1) + (-10)) / 10; 125 | data[401] = 27 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 126 | 127 | return data; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/ActivityWaterfullView.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.an.view.WaterfallView; 6 | 7 | import java.util.Random; 8 | 9 | public class ActivityWaterfullView extends BaseActivity { 10 | 11 | private WaterfallView _waterfallView; 12 | private final Random rand = new Random(); 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_waterfull_view); 18 | 19 | initView(); 20 | } 21 | 22 | private void initView() { 23 | _waterfallView = (WaterfallView) findViewById(R.id.waterfull_view_1); 24 | 25 | _runing = true; 26 | 27 | new Thread() { 28 | @Override 29 | public void run() { 30 | super.run(); 31 | 32 | while (_runing) { 33 | float[] data = getSpectremData(); 34 | 35 | _waterfallView.setData(101.7, 2000, data); 36 | 37 | try { 38 | Thread.sleep(50); 39 | } catch (InterruptedException e) { 40 | e.printStackTrace(); 41 | } 42 | } 43 | } 44 | }.start(); 45 | } 46 | 47 | private float[] getSpectremData() { 48 | int len = 801; 49 | float[] data = new float[len]; 50 | 51 | for (int i = 0; i < len; i++) { 52 | data[i] = (rand.nextInt(50 - (-150) + 1) + (-150)) / 10; 53 | } 54 | 55 | // data[49] = 27 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 56 | // data[50] = 47 + (rand.nextInt(10 - (-10) + 1) + (-10)) / 10; 57 | // data[51] = 27 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 58 | 59 | data[399] = 27 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 60 | data[400] = 47 + (rand.nextInt(10 - (-10) + 1) + (-10)) / 10; 61 | data[401] = 27 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 62 | 63 | // data[99] = 27 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 64 | // data[100] = 47 + (rand.nextInt(10 - (-10) + 1) + (-10)) / 10; 65 | // data[101] = 27 + (rand.nextInt(25 - (-25) + 1) + (-25)) / 10; 66 | 67 | // for (int i = 0; i < len; i++) { 68 | // data[i] = rand.nextInt(20 - (-20) + 1) + (-20); 69 | // } 70 | 71 | return data; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import android.os.Bundle; 4 | import android.view.MenuItem; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | public class BaseActivity extends AppCompatActivity { 10 | 11 | protected boolean _runing = false; 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 17 | } 18 | 19 | @Override 20 | protected void onDestroy() { 21 | super.onDestroy(); 22 | _runing = false; 23 | } 24 | 25 | @Override 26 | public boolean onOptionsItemSelected(@NonNull MenuItem item) { 27 | switch (item.getItemId()) { 28 | case android.R.id.home: 29 | finish(); 30 | break; 31 | 32 | default: 33 | break; 34 | } 35 | return super.onOptionsItemSelected(item); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.an.customview; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.widget.Button; 9 | 10 | 11 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | 18 | initView(); 19 | } 20 | 21 | private void initView() { 22 | Button btnScaleBar = (Button) findViewById(R.id.btn_scale_bar); 23 | btnScaleBar.setOnClickListener(this); 24 | Button btnCompassView = (Button) findViewById(R.id.btn_compass_view); 25 | btnCompassView.setOnClickListener(this); 26 | Button btnProgressBar = (Button) findViewById(R.id.btn_progress_bar); 27 | btnProgressBar.setOnClickListener(this); 28 | Button btnLevelStreamView = (Button) findViewById(R.id.btn_level_stream_view); 29 | btnLevelStreamView.setOnClickListener(this); 30 | Button btnSpectrumView = (Button) findViewById(R.id.btn_spectrum_view); 31 | btnSpectrumView.setOnClickListener(this); 32 | Button btnWaterfallView = (Button) findViewById(R.id.btn_waterfull_view); 33 | btnWaterfallView.setOnClickListener(this); 34 | Button btnBothView = (Button) findViewById(R.id.btn_both_view); 35 | btnBothView.setOnClickListener(this); 36 | Button btnCompassDf = (Button) findViewById(R.id.btn_compass_df); 37 | btnCompassDf.setOnClickListener(this); 38 | Button btnCircleProgressBar = (Button) findViewById(R.id.btn_circle_progress_bar); 39 | btnCircleProgressBar.setOnClickListener(this); 40 | Button btnRadarView = (Button) findViewById(R.id.btn_radar_view); 41 | btnRadarView.setOnClickListener(this); 42 | 43 | } 44 | 45 | @Override 46 | public void onClick(View view) { 47 | switch (view.getId()) { 48 | case R.id.btn_scale_bar: 49 | startActivity(new Intent(this, ActivityScaleBar.class)); 50 | break; 51 | case R.id.btn_compass_view: 52 | startActivity(new Intent(this, ActivityCompassView.class)); 53 | break; 54 | case R.id.btn_progress_bar: 55 | startActivity(new Intent(this, ActivityProgressBar.class)); 56 | break; 57 | case R.id.btn_level_stream_view: 58 | startActivity(new Intent(this, ActivityLevelStreamView.class)); 59 | break; 60 | case R.id.btn_spectrum_view: 61 | startActivity(new Intent(this, ActivitySpectrumView.class)); 62 | break; 63 | case R.id.btn_waterfull_view: 64 | startActivity(new Intent(this, ActivityWaterfullView.class)); 65 | break; 66 | case R.id.btn_both_view: 67 | startActivity(new Intent(this, ActivityGeneralSpectrumView.class)); 68 | break; 69 | case R.id.btn_compass_df: 70 | startActivity(new Intent(this, ActivityDFCompassView.class)); 71 | break; 72 | case R.id.btn_circle_progress_bar: 73 | startActivity(new Intent(this, ActivityCircleProcessBar.class)); 74 | break; 75 | case R.id.btn_radar_view: 76 | startActivity(new Intent(this, ActivityRadarView.class)); 77 | break; 78 | 79 | default: 80 | break; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/customview/readan.txt: -------------------------------------------------------------------------------- 1 | 打包步骤 2 | 3 | 1) build.gradle library // applicationId "com.an.customview" 4 | 5 | 2) MainActivity ActivityLevelStreamView 6 | 7 | 3) AndroidManifest.xml 注释 Application 8 | 9 | 4) terminal gradlew assembleRelease -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/CircleProcessBar.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @Title: CircleProcessBar.java 3 | * @Package: com.an.view 4 | * @Description: 自定义圆形进度条控件 5 | * @Author: AnuoF 6 | * @QQ/WeChat: 188512936 7 | * @Date 2019.08.27 22:36 8 | * @Version V1.0 9 | */ 10 | 11 | package com.an.view; 12 | 13 | import android.content.Context; 14 | import android.content.res.TypedArray; 15 | import android.graphics.Canvas; 16 | import android.graphics.Color; 17 | import android.graphics.Paint; 18 | import android.graphics.Rect; 19 | import android.util.AttributeSet; 20 | import android.view.View; 21 | 22 | import com.an.customview.R; 23 | 24 | /** 25 | * 自定义圆形进度条控件 26 | */ 27 | public class CircleProcessBar extends View { 28 | 29 | private int _fillColor; // 内圆填充色 30 | private int _circleColor; // 圆的颜色 31 | private int _scaleColor; // 圆上刻度县的颜色 32 | private int _fontSize; // 字体大小 33 | private int _margin; // 边距 34 | private int _fontColor; 35 | 36 | private int _currentValue; // 当前值 37 | 38 | private int _width; 39 | private int _height; 40 | private int _count; // 几等分 41 | private int _circleCount; 42 | 43 | private Paint _paint; 44 | 45 | 46 | public CircleProcessBar(Context context, AttributeSet attrs, int defStyle) { 47 | super(context, attrs, defStyle); 48 | initView(context, attrs); 49 | } 50 | 51 | public CircleProcessBar(Context context, AttributeSet attrs) { 52 | super(context, attrs); 53 | initView(context, attrs); 54 | } 55 | 56 | private void initView(Context context, AttributeSet attrs) { 57 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProcessBar); 58 | if (typedArray != null) { 59 | _fillColor = typedArray.getColor(R.styleable.CircleProcessBar_fillColor_CPB, 0); 60 | _circleColor = typedArray.getColor(R.styleable.CircleProcessBar_circleColor_CPB, Color.BLUE); 61 | _scaleColor = typedArray.getColor(R.styleable.CircleProcessBar_scaleColor_CPB, Color.GREEN); 62 | _fontSize = typedArray.getInt(R.styleable.CircleProcessBar_fontSize_CPB, 30); 63 | _margin = typedArray.getInt(R.styleable.CircleProcessBar_margin_CPB, 10); 64 | _fontColor = typedArray.getColor(R.styleable.CircleProcessBar_fontColor_CPB, Color.GREEN); 65 | 66 | _paint = new Paint(); 67 | _paint.setTextSize(_fontSize); 68 | _count = 5; 69 | _circleCount = 3; 70 | 71 | _currentValue = 50; 72 | 73 | } else { 74 | initView(); 75 | } 76 | } 77 | 78 | private void initView() { 79 | _fillColor = 0; 80 | _circleColor = Color.BLUE; 81 | _scaleColor = Color.GREEN; 82 | _fontSize = 30; 83 | _margin = 10; 84 | _fontColor = Color.GREEN; 85 | 86 | _paint = new Paint(); 87 | _paint.setTextSize(_fontSize); 88 | _count = 5; 89 | 90 | } 91 | 92 | public void setValue(int value) { 93 | if (value < 0) { 94 | _currentValue = 0; 95 | } else if (value > 100) { 96 | _currentValue = 100; 97 | } else { 98 | _currentValue = value; 99 | } 100 | 101 | postInvalidate(); 102 | } 103 | 104 | public int getValue() { 105 | return _currentValue; 106 | } 107 | 108 | @Override 109 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 110 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 111 | _width = getMeasuredWidth(); 112 | _height = getMeasuredHeight(); 113 | } 114 | 115 | @Override 116 | protected void onDraw(Canvas canvas) { 117 | drawCircle(canvas); 118 | drawScale(canvas); 119 | drawText(canvas); 120 | 121 | super.onDraw(canvas); 122 | } 123 | 124 | /** 125 | * 画圆和填充圆 126 | * 127 | * @param canvas 128 | */ 129 | private void drawCircle(Canvas canvas) { 130 | int px = _width / 2; 131 | int py = _height / 2; // 圆心 132 | int r = (Math.min(px, py) - _margin) * 3 / _count; 133 | 134 | _paint.setColor(_circleColor); 135 | _paint.setStyle(Paint.Style.STROKE); 136 | 137 | canvas.drawCircle(px, py, r, _paint); 138 | 139 | 140 | if (_fillColor != 0) { 141 | _paint.setColor(_fillColor); 142 | _paint.setStyle(Paint.Style.FILL); 143 | canvas.drawCircle(px, py, r, _paint); 144 | } 145 | } 146 | 147 | /** 148 | * 画刻度 149 | * 150 | * @param canvas 151 | */ 152 | private void drawScale(Canvas canvas) { 153 | _paint.setStyle(Paint.Style.STROKE); 154 | 155 | 156 | if (_currentValue <= 0) return; 157 | int px = _width / 2; 158 | int py = _height / 2; // 圆心 159 | int r = (Math.min(px, py) - _margin) * 3 / _count; 160 | 161 | canvas.translate(px, py); 162 | 163 | for (int i = 1; i <= _currentValue; i++) { 164 | canvas.rotate((float) 3.6); 165 | int len = 0; 166 | if (i % 10 == 0) { 167 | _paint.setColor(_circleColor); 168 | len = (Math.min(px, py) - _margin); 169 | } else { 170 | _paint.setColor(_scaleColor); 171 | len = (Math.min(px, py) - _margin) * 4 / _count; 172 | } 173 | 174 | canvas.drawLine(0, -r, 0, -len, _paint); 175 | } 176 | 177 | canvas.rotate(-(_currentValue * (float) 3.6)); 178 | canvas.translate(-px, -py); 179 | } 180 | 181 | private void drawText(Canvas canvas) { 182 | _paint.setColor(_fontColor); 183 | _paint.setTextSize(_fontSize); 184 | 185 | int px = _width / 2; 186 | int py = _height / 2; // 圆心 187 | 188 | String text = _currentValue + " %"; 189 | Rect textRect = new Rect(); 190 | _paint.getTextBounds(text, 0, text.length(), textRect); 191 | canvas.drawText(text, px - textRect.width() / 2, py - textRect.height() / 2, _paint); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/CompassView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @Title: CompassView.java 3 | * @Package: com.an.view 4 | * @Description: 指南针控件 5 | * @Author: AnuoF 6 | * @QQ/WeChat: 188512936 7 | * @Date: 2019.08.02 08:23 8 | * @Version: V1.0 9 | */ 10 | 11 | package com.an.view; 12 | 13 | import android.content.Context; 14 | import android.content.res.TypedArray; 15 | import android.graphics.Bitmap; 16 | import android.graphics.BitmapFactory; 17 | import android.graphics.Canvas; 18 | import android.graphics.Color; 19 | import android.graphics.Paint; 20 | import android.util.AttributeSet; 21 | import android.view.View; 22 | 23 | import com.an.customview.R; 24 | 25 | import java.text.ParseException; 26 | import java.text.SimpleDateFormat; 27 | import java.util.Date; 28 | 29 | /** 30 | * 自定义罗盘、指南针控件,用于显示方位角 31 | */ 32 | public class CompassView extends View { 33 | 34 | private float _bearing = 0; // 显示的方向 35 | private Paint _markerPaint; 36 | private Paint _textPaint; 37 | private Paint _circlePaint; 38 | private Paint _centerCirclePaint; 39 | 40 | private final String _northString = "360"; 41 | private final String _eastString = "90"; 42 | private final String _southString = "180"; 43 | private final String _westString = "270"; 44 | 45 | private float _angle; // 度,在绘制时需要转成 弧度 46 | private float _angleOut; 47 | 48 | // 绘图计算用 49 | private int _textHeight; 50 | private int _measureWidth; 51 | private int _measureHeight; 52 | private int _px; 53 | private int _py; 54 | private int _radius; 55 | 56 | public CompassView(Context context) { 57 | super(context); 58 | initView(); 59 | } 60 | 61 | public CompassView(Context context, AttributeSet attrs) { 62 | super(context, attrs); 63 | initView(context, attrs); 64 | } 65 | 66 | public CompassView(Context context, AttributeSet attrs, int defStyle) { 67 | super(context, attrs, defStyle); 68 | initView(context, attrs); 69 | } 70 | 71 | @Override 72 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 73 | //super.onMeasure(widthMeasureSpec, heightMeasureSpec); 74 | // 指南针是一个尽可能填充更多空间的园,通过设置最短的边界,高度或者宽度来设置测量尺寸 75 | int measureWidth = measure(widthMeasureSpec); 76 | int measureHeight = measure(heightMeasureSpec); 77 | 78 | int d = Math.min(measureWidth, measureHeight); 79 | setMeasuredDimension(d, d); 80 | 81 | _measureWidth = getMeasuredWidth(); 82 | _measureHeight = getMeasuredHeight(); 83 | 84 | _px = _measureWidth / 2; 85 | _py = _measureHeight / 2; 86 | _radius = Math.min(_px, _py); // 取最小值为半径 87 | } 88 | 89 | @Override 90 | protected void onDraw(Canvas canvas) { 91 | boolean valid = Utils.checkValid(); 92 | if (valid) { 93 | drawCircle(canvas); 94 | drawScaleText(canvas); 95 | drawCenterCircleAndDirectionLine(canvas); 96 | 97 | super.onDraw(canvas); 98 | } 99 | } 100 | 101 | /** 102 | * 设置角度,单位度 103 | * 104 | * @param angle 105 | */ 106 | public void setAngle(float angle) { 107 | _angleOut = angle; 108 | this._angle = angle * 2 * (float) Math.PI / 360; 109 | postInvalidate(); 110 | } 111 | 112 | /** 113 | * 获取当前角度,单位度 114 | * 115 | * @return 116 | */ 117 | public float getAngle() { 118 | return _angleOut; 119 | } 120 | 121 | /** 122 | * 初始控件 123 | */ 124 | private void initView() { 125 | _markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 126 | _markerPaint.setColor(Color.GREEN); 127 | 128 | _circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 129 | _circlePaint.setColor(Color.argb(100, 0, 255, 0)); 130 | _circlePaint.setStrokeWidth(1); 131 | _circlePaint.setStyle(Paint.Style.FILL_AND_STROKE); 132 | 133 | _centerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 134 | _centerCirclePaint.setColor(Color.RED); 135 | _centerCirclePaint.setStrokeWidth(2); 136 | _centerCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE); 137 | 138 | _textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 139 | _textPaint.setColor(Color.GREEN); 140 | _textHeight = (int) _textPaint.measureText("yY"); 141 | } 142 | 143 | /** 144 | * 初始化控件 145 | * 146 | * @param context 147 | * @param attrs 148 | */ 149 | private void initView(Context context, AttributeSet attrs) { 150 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CompassView); 151 | if (typedArray != null) { 152 | _markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 153 | _markerPaint.setColor(typedArray.getColor(R.styleable.CompassView_marker_color_cv, Color.GREEN)); 154 | 155 | _circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 156 | _circlePaint.setColor(typedArray.getColor(R.styleable.CompassView_circle_color_cv, Color.argb(100, 0, 255, 0))); 157 | _circlePaint.setStrokeWidth(1); 158 | _circlePaint.setStyle(Paint.Style.FILL_AND_STROKE); 159 | 160 | _centerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 161 | _centerCirclePaint.setColor(typedArray.getColor(R.styleable.CompassView_center_circle_color_cv, Color.RED)); 162 | _centerCirclePaint.setStrokeWidth(2); 163 | _centerCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE); 164 | 165 | _textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 166 | _textPaint.setColor(typedArray.getColor(R.styleable.CompassView_text_color_cv, Color.GREEN)); 167 | _textHeight = (int) _textPaint.measureText("yY"); 168 | } 169 | } 170 | 171 | /** 172 | * 解码数据值 173 | * 174 | * @param measureSpec 175 | * @return 176 | */ 177 | private int measure(int measureSpec) { 178 | int result = 0; 179 | //对测量说明进行解码 180 | int specMode = MeasureSpec.getMode(measureSpec); 181 | int specSize = MeasureSpec.getSize(measureSpec); 182 | //如果没有指定界限,则返回默认大小200 183 | if (specMode == MeasureSpec.UNSPECIFIED) { 184 | result = 200; 185 | } else { 186 | //由于是希望填充可用的空间,所以总是返回整个可用的边界 187 | result = specSize; 188 | } 189 | return result; 190 | } 191 | 192 | /** 193 | * 绘制背景园 194 | * 195 | * @param canvas 196 | */ 197 | private void drawCircle(Canvas canvas) { 198 | // 绘制背景圆 199 | canvas.drawCircle(_px, _py, _radius, _circlePaint); 200 | canvas.save(); 201 | canvas.rotate(_bearing, _px, _py); 202 | } 203 | 204 | /** 205 | * 绘制刻度和文本 206 | * 207 | * @param canvas 208 | */ 209 | private void drawScaleText(Canvas canvas) { 210 | _textPaint.setTextSize(22); 211 | int textWidth = (int) _textPaint.measureText("W"); 212 | int cadinalX = _px - textWidth / 2; 213 | int cadinalY = _py - _radius + _textHeight; 214 | 215 | // 每15度绘制一个标记,每45度绘制一个文本 216 | for (int i = 0; i < 24; i++) { 217 | // 绘制一个标记 218 | canvas.drawLine(_px, _px - _radius, _py, _py - _radius + 10, _markerPaint); 219 | canvas.save(); 220 | canvas.translate(0, _textHeight); 221 | 222 | // // 绘制基本方位 223 | // if (i % 6 == 0) { 224 | // String dirString = ""; 225 | // switch (i) { 226 | // case 0: 227 | // dirString = _northString; 228 | // int arrowY = 2 * _textHeight; 229 | // canvas.drawLine(_px, arrowY, _px - 5, 3 * _textHeight, _markerPaint); 230 | // canvas.drawLine(_px, arrowY, _px + 5, 3 * _textHeight, _markerPaint); 231 | // canvas.drawLine(_px - 5, 3 * _textHeight, _px + 5, 3 * _textHeight, _markerPaint); 232 | // break; 233 | // case 6: 234 | // dirString = _eastString; 235 | // break; 236 | // case 12: 237 | // dirString = _southString; 238 | // break; 239 | // case 18: 240 | // dirString = _westString; 241 | // break; 242 | // default: 243 | // dirString = _westString; 244 | // break; 245 | // } 246 | // canvas.drawText(dirString, cadinalX - _textPaint.measureText(dirString) / 2, cadinalY, _textPaint); 247 | // } else if (i % 3 == 0) { 248 | 249 | if (i % 3 == 0) { 250 | // 每个45度绘制文本 251 | String angle = String.valueOf(i * 15); 252 | float angleTextWidth = _textPaint.measureText(angle); 253 | 254 | int angleTextX = (int) (_px - angleTextWidth / 2); 255 | int angelTextY = _py - _radius + _textHeight; 256 | canvas.drawText(angle, angleTextX, angelTextY, _textPaint); 257 | } 258 | canvas.restore(); 259 | canvas.rotate(15, _px, _py); 260 | } 261 | } 262 | 263 | /** 264 | * 绘制中心圆和示向线 265 | * 266 | * @param canvas 267 | */ 268 | private void drawCenterCircleAndDirectionLine(Canvas canvas) { 269 | // canvas.drawCircle(_px, _py, _radius / 20, _centerCirclePaint); 270 | 271 | int centerRadius = _radius / 5 * 4; 272 | double x = _px; 273 | double y = _py; 274 | 275 | if (_angle == 0) { 276 | x = _px; 277 | y = _py - centerRadius; 278 | } else if (_angle == 90) { 279 | x = _px + centerRadius; 280 | y = _py; 281 | } else if (_angle == 180) { 282 | x = _px; 283 | y = _py + centerRadius; 284 | } else if (_angle == 270) { 285 | x = _px - centerRadius; 286 | y = _py; 287 | } else if (_angle == 360) { 288 | x = _px; 289 | y = _py - centerRadius; 290 | } else if (_angle > 0 && _angle < 90) { 291 | // 第一象限 292 | double x1 = Math.sin(_angle) * centerRadius; 293 | double y1 = Math.cos(_angle) * centerRadius; 294 | x = _px + x1; 295 | y = _py - y1; 296 | } else if (_angle > 90 && _angle < 180) { 297 | // 第二象限 298 | float x1 = (float) Math.sin(180 - _angle) * centerRadius; 299 | float y1 = (float) Math.cos(180 - _angle) * centerRadius; 300 | x = _px + x1; 301 | y = _py + y1; 302 | } else if (_angle > 180 && _angle < 270) { 303 | // 第三象限 304 | float x1 = (float) Math.sin(_angle - 180) * centerRadius; 305 | float y1 = (float) Math.cos(_angle - 180) * centerRadius; 306 | x = _px - x1; 307 | y = _py + y1; 308 | } else if (_angle > 270 && _angle < 360) { 309 | // 第四象限 310 | float x1 = (float) Math.sin(360 - _angle) * centerRadius; 311 | float y1 = (float) Math.cos(360 - _angle) * centerRadius; 312 | x = _px - x1; 313 | y = _py - y1; 314 | } 315 | 316 | _centerCirclePaint.setStrokeWidth(6f); 317 | canvas.drawLine((float) _px, (float) _py, (float) x, (float) y, _centerCirclePaint); 318 | canvas.restore(); 319 | 320 | Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.car); 321 | canvas.drawBitmap(bitmap, _px - bitmap.getWidth() / 2, _py - bitmap.getHeight() / 2, _centerCirclePaint); 322 | bitmap.recycle(); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/DFCompassView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @Title: DFCompassView.java 3 | * @Package: com.an.view 4 | * @Description: 自定义侧向罗盘控件 5 | * @Author: AnuoF 6 | * @QQ/WeChat: 188512936 7 | * @Date 2019.08.24 08:27 8 | * @Version V1.0 9 | */ 10 | 11 | package com.an.view; 12 | 13 | import android.content.Context; 14 | import android.graphics.Bitmap; 15 | import android.graphics.BitmapFactory; 16 | import android.graphics.Canvas; 17 | import android.graphics.Color; 18 | import android.graphics.Paint; 19 | import android.graphics.Path; 20 | import android.graphics.PorterDuff; 21 | import android.graphics.PorterDuffXfermode; 22 | import android.graphics.Rect; 23 | import android.graphics.RectF; 24 | import android.os.Build; 25 | import android.util.AttributeSet; 26 | import android.view.View; 27 | 28 | import androidx.annotation.RequiresApi; 29 | 30 | import com.an.customview.R; 31 | 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | import java.util.concurrent.ExecutorService; 35 | import java.util.concurrent.Executors; 36 | 37 | /** 38 | * 自定义侧向罗盘控件 39 | */ 40 | public class DFCompassView extends View { 41 | private int _neswColor; // NESW颜色 42 | private int _neswSize; // NESW字体大小 43 | private ViewMode _viewMode; // 视图模式 44 | private NorthMode _nothMode; // 正北示向度 45 | private int _crossLineColor; // 十字交叉线条颜色 46 | private int _minScaleLineLength; // 短刻度线长度 47 | private int _maxScaleLineLength; // 长刻度线的长度 48 | private int _dataSize; // 数据长度 缓存多少个点 49 | private int _scaleLineColor; 50 | private int _scaleCircleColor; 51 | 52 | private boolean _initFinished; 53 | private int _width; 54 | private int _height; 55 | 56 | private Bitmap _bitmap; 57 | private Canvas _canvas; 58 | private Paint _mPaint; 59 | private Paint _paint; 60 | 61 | private List _dataList = new ArrayList<>(); 62 | 63 | private static final Object _lockObj = new Object(); // 互斥锁 64 | private ExecutorService _executorService; 65 | 66 | 67 | public DFCompassView(Context context, AttributeSet attrs, int defStyle) { 68 | super(context, attrs, defStyle); 69 | initView(); 70 | } 71 | 72 | public DFCompassView(Context context, AttributeSet attrs) { 73 | super(context, attrs); 74 | initView(); 75 | } 76 | 77 | private void initView() { 78 | _neswColor = Color.WHITE; 79 | _neswSize = 40; 80 | _viewMode = ViewMode.CLOCK; 81 | _nothMode = NorthMode.NORTH; 82 | _crossLineColor = Color.WHITE; 83 | _minScaleLineLength = 10; 84 | _maxScaleLineLength = 20; 85 | _dataSize = 10; 86 | _scaleLineColor = Color.GREEN; 87 | _scaleCircleColor = Color.argb(200, 15, 187, 249); 88 | 89 | _initFinished = false; 90 | _paint = new Paint(); 91 | _mPaint = new Paint(); 92 | _executorService = Executors.newFixedThreadPool(1); 93 | } 94 | 95 | @Override 96 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 97 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 98 | _width = getMeasuredWidth(); 99 | _height = getMeasuredHeight(); 100 | _initFinished = true; 101 | } 102 | 103 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 104 | @Override 105 | protected void onDraw(Canvas canvas) { 106 | synchronized (_lockObj) { 107 | if (_bitmap != null) { 108 | canvas.drawBitmap(_bitmap, 0, 0, _paint); 109 | } else { 110 | drawScale(canvas); 111 | drawOutsideCircle(canvas); 112 | drawCar(canvas); 113 | } 114 | } 115 | 116 | super.onDraw(canvas); 117 | } 118 | 119 | /** 120 | * @param azimuth 示向度 121 | * @param quality 质量 122 | * @param compassAngle 罗盘方位 123 | */ 124 | public void setData(final float azimuth, final float quality, final float compassAngle) { 125 | if (_initFinished == false) 126 | return; 127 | 128 | if (_dataList.size() >= _dataSize) { 129 | _dataList.remove(0); 130 | } 131 | _dataList.add(new float[]{azimuth, quality, compassAngle}); 132 | 133 | _executorService.execute(new Runnable() { 134 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 135 | @Override 136 | public void run() { 137 | synchronized (_lockObj) { 138 | draw(azimuth, quality, compassAngle); 139 | postInvalidate(); 140 | } 141 | } 142 | }); 143 | } 144 | 145 | public void setViewMode(ViewMode viewMode) { 146 | if (_viewMode == viewMode) 147 | return; 148 | 149 | _viewMode = viewMode; 150 | postInvalidate(); 151 | } 152 | 153 | public void setNorthMode(NorthMode northMode) { 154 | if (_nothMode == northMode) 155 | return; 156 | 157 | _nothMode = northMode; 158 | postInvalidate(); 159 | } 160 | 161 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 162 | private void draw(float azimuth, float quality, float compassAngle) { 163 | if (_bitmap == null) { 164 | _bitmap = Bitmap.createBitmap(_width, _height, Bitmap.Config.ARGB_8888); 165 | _canvas = new Canvas(_bitmap); 166 | } else { 167 | // 清屏 168 | Paint p = new Paint(); 169 | p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 170 | _canvas.drawPaint(p); 171 | p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); 172 | } 173 | 174 | drawScale(_canvas); 175 | drawOutsideCircle(_canvas); 176 | drawAzimuthLine(_canvas); 177 | drawCar(_canvas); 178 | 179 | postInvalidate(); // 在同一类中使用不需要回调 180 | } 181 | 182 | /** 183 | * 画刻度 184 | * 185 | * @param canvas 画布,可能是系统画布或者离屏画布 186 | */ 187 | private void drawScale(Canvas canvas) { 188 | int px = _width / 2; 189 | int py = _height / 2; // 中心点(原点) 画图均以中心点作为参考 190 | int r = Math.min(_width, _height) / 2; // 直径,取较小的值 191 | int minR = r / 5 * 3; 192 | 193 | _mPaint.setStyle(Paint.Style.STROKE); 194 | _mPaint.setTextSize(20); 195 | _mPaint.setColor(_scaleLineColor); 196 | 197 | canvas.translate(px, py); 198 | 199 | if (_viewMode == ViewMode.COMPASS) { 200 | // 罗盘视图 201 | for (int i = 0; i < 40; i++) { 202 | if (i % 5 == 0) { 203 | _mPaint.setStrokeWidth(2); 204 | canvas.drawLine(0, -minR, 0, -(minR - _maxScaleLineLength), _mPaint); 205 | String text = i == 0 ? "360" : (i) * 9 + ""; 206 | Rect textRect = new Rect(); 207 | _mPaint.getTextBounds(text, 0, text.length(), textRect); 208 | canvas.drawText(text, 0 - textRect.width() / 2, -minR + textRect.height() + _maxScaleLineLength, _mPaint); 209 | } else { 210 | _mPaint.setStrokeWidth(1); 211 | canvas.drawLine(0, -minR, 0, -(minR - _minScaleLineLength), _mPaint); 212 | } 213 | canvas.save(); 214 | canvas.restore(); 215 | canvas.rotate(9); 216 | } 217 | } else { 218 | // 钟表视图 219 | for (int i = 0; i < 60; i++) { 220 | if (i % 5 == 0) { 221 | _mPaint.setStrokeWidth(2); 222 | canvas.drawLine(0, -minR, 0, -(minR - _maxScaleLineLength), _mPaint); 223 | String text = i == 0 ? "12" : (int) (i * 0.2) + ""; 224 | Rect textRect = new Rect(); 225 | _mPaint.getTextBounds(text, 0, text.length(), textRect); 226 | canvas.drawText(text, 0 - textRect.width() / 2, -minR + textRect.height() + _maxScaleLineLength, _mPaint); 227 | } else { 228 | _mPaint.setStrokeWidth(1); 229 | canvas.drawLine(0, -minR, 0, -(minR - _minScaleLineLength), _mPaint); 230 | } 231 | 232 | canvas.save(); 233 | canvas.restore(); 234 | canvas.rotate(6); 235 | } 236 | } 237 | 238 | canvas.translate(-px, -py); 239 | 240 | _mPaint.setColor(_scaleCircleColor); 241 | _mPaint.setStrokeWidth(2); 242 | canvas.drawCircle(px, py, minR, _mPaint); 243 | } 244 | 245 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 246 | private void drawOutsideCircle(Canvas canvas) { 247 | float angle = 0; 248 | if (_nothMode == NorthMode.CAR_HEAD && _dataList.size() > 0) { 249 | angle = _dataList.get(_dataList.size() - 1)[2]; 250 | } 251 | 252 | int px = _width / 2; 253 | int py = _height / 2; // 中心点(原点) 画图均以中心点作为参考 254 | int r = Math.min(_width, _height) / 2; // 半径,取较小的值 255 | 256 | int maxR = r / 5 * 4; 257 | 258 | canvas.translate(px, py); 259 | canvas.rotate(angle); 260 | 261 | _mPaint.setColor(Color.RED); 262 | canvas.drawArc(new RectF(-maxR, -maxR, maxR, maxR), -90, 180, false, _mPaint); 263 | _mPaint.setColor(Color.BLUE); 264 | canvas.drawArc(new RectF(-maxR, -maxR, maxR, maxR), 90, 180, false, _mPaint); 265 | 266 | _mPaint.setTextSize(_neswSize); 267 | _mPaint.setColor(_neswColor); 268 | _mPaint.setStyle(Paint.Style.STROKE); 269 | Rect neswRect = new Rect(); 270 | _mPaint.getTextBounds("N", 0, "N".length(), neswRect); 271 | int size = Math.max(neswRect.height(), neswRect.width()); 272 | 273 | _mPaint.setStyle(Paint.Style.FILL_AND_STROKE); 274 | _mPaint.setColor(_crossLineColor); 275 | canvas.drawText("N", 0 - neswRect.width() / 2, -r + size, _mPaint); 276 | canvas.drawText("E", r - size, 0 + neswRect.height() / 2, _mPaint); 277 | canvas.drawText("S", -neswRect.width() / 2, r, _mPaint); 278 | canvas.drawText("W", -r - size + neswRect.width(), 0 + neswRect.height() / 2, _mPaint); 279 | 280 | canvas.drawLine(0, 0 - r + size, 0, 0 + r - size, _mPaint); // 纵轴 281 | canvas.drawLine(0 - r + size, 0, 0 + r - size, 0, _mPaint); // 横轴 282 | 283 | canvas.save(); 284 | canvas.restore(); 285 | canvas.rotate(-angle); 286 | canvas.translate(-px, -py); 287 | } 288 | 289 | /** 290 | * 画示向线 291 | */ 292 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 293 | private void drawAzimuthLine(Canvas canvas) { 294 | if (_dataList.size() <= 0) 295 | return; 296 | 297 | int px = _width / 2; 298 | int py = _height / 2; // 中心点(原点) 画图均以中心点作为参考 299 | int r = Math.min(_width, _height) / 2; // 半径,取较小的值 300 | int minR = r / 5 * 3; 301 | int maxR = r / 5 * 4; 302 | canvas.translate(px, py); 303 | 304 | for (int i = 0; i < _dataList.size(); i++) { 305 | float quality = _dataList.get(i)[1]; 306 | float azimuth = _dataList.get(i)[0]; 307 | canvas.rotate(azimuth); 308 | int len = (int) (minR * (quality / 100)); 309 | canvas.drawLine(0, 0, 0, -len, _mPaint); 310 | canvas.rotate(-azimuth); 311 | } 312 | 313 | MaxMinClass maxMin = new MaxMinClass(); 314 | getMaxMin(maxMin); 315 | 316 | _mPaint.setStyle(Paint.Style.FILL); 317 | _mPaint.setColor(Color.argb(100, 0, 255, 0)); 318 | canvas.drawArc(-minR, -minR, minR, minR, maxMin.getMin() - 90, maxMin.getMax() - maxMin.getMin(), true, _mPaint); // 需要 -90 319 | 320 | float optimalAzimuth = maxMin.getOptimalAzimuth(); 321 | float optimalQuality = maxMin.getOptimalQuality(); 322 | 323 | // 最优值 324 | canvas.rotate(optimalAzimuth); 325 | _mPaint.setColor(Color.RED); 326 | _mPaint.setStrokeWidth(2); 327 | canvas.drawLine(0, 0, 0, -minR * (optimalQuality / 100), _mPaint); 328 | canvas.rotate(-optimalAzimuth); 329 | 330 | // 实时值 331 | float azimuth = _dataList.get(_dataList.size() - 1)[0]; 332 | // 实时值 333 | canvas.rotate(azimuth); 334 | 335 | Path path = new Path(); 336 | path.moveTo(0, -minR); 337 | path.lineTo((maxR - minR) / 2,- maxR); 338 | path.lineTo(-(maxR - minR) / 2, -maxR); 339 | _mPaint.setStyle(Paint.Style.FILL); 340 | canvas.drawPath(path, _mPaint); 341 | 342 | canvas.rotate(-azimuth); 343 | canvas.translate(-px, -py); 344 | } 345 | 346 | /** 347 | * 画车 348 | */ 349 | private void drawCar(Canvas canvas) { 350 | int px = _width / 2; 351 | int py = _height / 2; // 中心点(原点) 画图均以中心点作为参考 352 | 353 | Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.car); 354 | 355 | if (_nothMode == NorthMode.CAR_HEAD) { 356 | canvas.drawBitmap(bitmap, px - bitmap.getWidth() / 2, py - bitmap.getHeight() / 2, _paint); 357 | } else if (_dataList.size() > 0) { 358 | float angle = _dataList.get(_dataList.size() - 1)[2]; 359 | canvas.translate(px, py); 360 | canvas.rotate(angle); 361 | canvas.drawBitmap(bitmap, 0 - bitmap.getWidth() / 2, 0 - bitmap.getHeight() / 2, _paint); 362 | canvas.rotate(-angle); 363 | canvas.translate(-px, -py); 364 | } 365 | 366 | bitmap.recycle(); 367 | } 368 | 369 | private void getMaxMin(MaxMinClass maxMin) { 370 | float max = _dataList.get(0)[0]; // 最大值 371 | float min = _dataList.get(0)[0]; // 最小值 372 | float optimalQuality = _dataList.get(0)[1]; // 最优值的质量 373 | float optimalAzimuth = _dataList.get(0)[0]; 374 | 375 | for (int i = 1; i < _dataList.size(); i++) { 376 | float azimuth = _dataList.get(i)[0]; 377 | if (max < azimuth) { 378 | max = azimuth; 379 | } 380 | if (min > azimuth) { 381 | min = azimuth; 382 | } 383 | if (optimalQuality < _dataList.get(i)[1]) { 384 | optimalQuality = _dataList.get(i)[1]; 385 | optimalAzimuth = _dataList.get(i)[0]; 386 | } 387 | } 388 | 389 | maxMin.setMax(max); 390 | maxMin.setMin(min); 391 | maxMin.setOptimalQuality(optimalQuality); 392 | maxMin.set0ptimalAzimuth(optimalAzimuth); 393 | } 394 | 395 | private class MaxMinClass { 396 | private float max; 397 | private float min; 398 | private float optimalAzimuth; // 最优值的事示向度 399 | private float optimalQuality; // 最优值的质量 400 | 401 | public MaxMinClass() { 402 | 403 | } 404 | 405 | public void setMax(float max) { 406 | this.max = max; 407 | } 408 | 409 | public float getMax() { 410 | return max; 411 | } 412 | 413 | public void setMin(float min) { 414 | this.min = min; 415 | } 416 | 417 | public float getMin() { 418 | return min; 419 | } 420 | 421 | public void set0ptimalAzimuth(float azimuth) { 422 | this.optimalAzimuth = azimuth; 423 | } 424 | 425 | public float getOptimalAzimuth() { 426 | return optimalAzimuth; 427 | } 428 | 429 | public void setOptimalQuality(float quality) { 430 | optimalQuality = quality; 431 | } 432 | 433 | public float getOptimalQuality() { 434 | return optimalQuality; 435 | } 436 | } 437 | 438 | 439 | /** 440 | * 视图模式 441 | */ 442 | public enum ViewMode { 443 | COMPASS, // 方位视图 444 | CLOCK // 钟表视图 445 | } 446 | 447 | /** 448 | * 正北模式 449 | */ 450 | public enum NorthMode { 451 | NORTH, // 正北示向度 452 | CAR_HEAD // 相对车头 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/GradientColorDialog.java: -------------------------------------------------------------------------------- 1 | package com.an.view; 2 | 3 | import android.app.Activity; 4 | import android.app.Dialog; 5 | import android.content.Context; 6 | import android.os.Bundle; 7 | import android.view.Display; 8 | import android.view.Gravity; 9 | import android.view.View; 10 | import android.view.Window; 11 | import android.view.WindowManager; 12 | 13 | import com.an.customview.R; 14 | 15 | class GradientColorDialog extends Dialog implements View.OnClickListener { 16 | 17 | private Context _context; // 上下文 18 | private int _layoutResId; // 布局文件Id 19 | private int[] _listenItemId; // 监听的控件Id 20 | private OnItemClickListener _listener; 21 | 22 | public GradientColorDialog(Context context, int layoutResId, int[] listenItemId) { 23 | super(context, R.style.MyDialog); 24 | 25 | _context = context; 26 | _layoutResId = layoutResId; 27 | _listenItemId = listenItemId; 28 | } 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | 34 | Window dialogWindow = getWindow(); 35 | dialogWindow.setGravity(Gravity.CENTER); // 居中显示 36 | setContentView(_layoutResId); 37 | 38 | WindowManager windowManager = ((Activity) _context).getWindowManager(); 39 | Display display = windowManager.getDefaultDisplay(); 40 | WindowManager.LayoutParams lp = getWindow().getAttributes(); 41 | lp.width = display.getWidth() * 3 / 5; 42 | getWindow().setAttributes(lp); 43 | setCanceledOnTouchOutside(false); 44 | for (int id : _listenItemId) { 45 | findViewById(id).setOnClickListener(this); 46 | } 47 | } 48 | 49 | @Override 50 | public void onClick(View view) { 51 | dismiss(); //注意:我在这里加了这句话,表示只要按任何一个控件的id,弹窗都会消失,不管是确定还是取消。 52 | _listener.OnClick(this, view); 53 | } 54 | 55 | public interface OnItemClickListener { 56 | void OnClick(GradientColorDialog dialog, View view); 57 | } 58 | 59 | public void setOnItemClickListener(OnItemClickListener listener) { 60 | _listener = listener; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/GradientColorView.java: -------------------------------------------------------------------------------- 1 | package com.an.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.LinearGradient; 8 | import android.graphics.Paint; 9 | import android.graphics.Shader; 10 | import android.util.AttributeSet; 11 | import android.view.View; 12 | 13 | import com.an.customview.R; 14 | 15 | class GradientColorView extends View { 16 | 17 | public int _index; 18 | private int[] _colors; 19 | 20 | private int _width; 21 | private int _height; 22 | private Paint _paint; 23 | 24 | public GradientColorView(Context context, AttributeSet attrs, int defStyle) { 25 | super(context, attrs, defStyle); 26 | initView(context, attrs); 27 | } 28 | 29 | public GradientColorView(Context context, AttributeSet attrs) { 30 | super(context, attrs); 31 | initView(context, attrs); 32 | } 33 | 34 | public GradientColorView(Context context) { 35 | super(context); 36 | initView(); 37 | } 38 | 39 | private void initView(Context context, AttributeSet attrs) { 40 | TypedArray typedArray = context.obtainStyledAttributes(R.styleable.GradientColorView); 41 | if (typedArray != null) { 42 | _paint = new Paint(); 43 | _index = typedArray.getInt(R.styleable.GradientColorView_gradientColors, 0); 44 | initColors(); 45 | } else { 46 | _paint = new Paint(); 47 | _index = 0; 48 | initColors(); 49 | } 50 | } 51 | 52 | private void initView() { 53 | _paint = new Paint(); 54 | _index = 0; 55 | initColors(); 56 | } 57 | 58 | private void initColors() { 59 | if (_index < 0) { 60 | _index = 0; 61 | } else if (_index > 3) { 62 | _index = 3; 63 | } 64 | 65 | switch (_index) { 66 | case 0: 67 | _colors = new int[]{ 68 | // RED 69 | Color.rgb(217, 67, 54), 70 | Color.rgb(224, 102, 80), 71 | Color.rgb(230, 132, 102), 72 | Color.rgb(238, 170, 128), 73 | Color.rgb(248, 222, 167), 74 | Color.rgb(236, 236, 177), 75 | Color.rgb(172, 172, 132), 76 | Color.rgb(161, 161, 125), 77 | Color.rgb(129, 129, 102), 78 | Color.rgb(114, 114, 90), 79 | Color.rgb(85, 85, 70), 80 | Color.rgb(55, 55, 49), 81 | Color.rgb(38, 38, 37)}; 82 | break; 83 | case 1: 84 | _colors = new int[]{ 85 | // GREEN 86 | Color.rgb(32, 206, 38), 87 | Color.rgb(29, 213, 79), 88 | Color.rgb(24, 225, 145), 89 | Color.rgb(21, 231, 183), 90 | Color.rgb(18, 238, 222), 91 | Color.rgb(17, 233, 225), 92 | Color.rgb(21, 185, 179), 93 | Color.rgb(23, 155, 150), 94 | Color.rgb(25, 133, 129), 95 | Color.rgb(28, 104, 101), 96 | Color.rgb(29, 81, 79), 97 | Color.rgb(31, 53, 52), 98 | Color.rgb(32, 48, 48) 99 | }; 100 | break; 101 | case 2: 102 | _colors = new int[]{ 103 | // BLUE 104 | Color.rgb(233, 0, 244), 105 | Color.rgb(212, 0, 244), 106 | Color.rgb(154, 0, 244), 107 | Color.rgb(124, 0, 244), 108 | Color.rgb(81, 0, 244), 109 | Color.rgb(68, 0, 244), 110 | Color.rgb(32, 0, 244), 111 | Color.rgb(23, 7, 199), 112 | Color.rgb(25, 12, 166), 113 | Color.rgb(27, 18, 132), 114 | Color.rgb(29, 23, 97), 115 | Color.rgb(30, 26, 77), 116 | Color.rgb(32, 30, 51) 117 | }; 118 | break; 119 | case 3: 120 | _colors = new int[]{ 121 | // COLOR 122 | Color.rgb(208, 26, 1), 123 | Color.rgb(221, 105, 1), 124 | Color.rgb(237, 206, 1), 125 | Color.rgb(184, 227, 1), 126 | Color.rgb(122, 231, 1), 127 | Color.rgb(30, 236, 1), 128 | Color.rgb(28, 236, 72), 129 | Color.rgb(47, 234, 131), 130 | Color.rgb(71, 206, 197), 131 | Color.rgb(57, 143, 137), 132 | Color.rgb(45, 91, 88), 133 | Color.rgb(46, 96, 93), 134 | Color.rgb(36, 47, 47), 135 | }; 136 | break; 137 | } 138 | } 139 | 140 | @Override 141 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 142 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 143 | 144 | _width = getMeasuredWidth(); 145 | _height = getMeasuredHeight(); 146 | } 147 | 148 | @Override 149 | protected void onDraw(Canvas canvas) { 150 | drawGradientColor(canvas); 151 | super.onDraw(canvas); 152 | } 153 | 154 | private void drawGradientColor(Canvas canvas) { 155 | LinearGradient linearGradient = new LinearGradient(0, 0, 0, _height, _colors, null, Shader.TileMode.CLAMP); 156 | _paint.setShader(linearGradient); 157 | canvas.drawRect(0, 0, _width, _height, _paint); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/OnDrawFinishedListener.java: -------------------------------------------------------------------------------- 1 | package com.an.view; 2 | 3 | /** 4 | * 图形绘制完成回调接口 5 | */ 6 | interface OnDrawFinishedListener { 7 | 8 | /** 9 | * 图形绘制完成 10 | */ 11 | void onDrawFinished(); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/ProgressBar.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @Title: ProgressBar.java 3 | * @Package: com.an.view 4 | * @Description: 自定义进度条控件 5 | * @Author: AnuoF 6 | * @QQ/WeChat: 188512936 7 | * @Date: 2019.08.08 14:05 8 | * @Version: V1.0 9 | */ 10 | 11 | package com.an.view; 12 | 13 | import android.content.Context; 14 | import android.content.res.TypedArray; 15 | import android.graphics.Canvas; 16 | import android.graphics.Color; 17 | import android.graphics.Paint; 18 | import android.util.AttributeSet; 19 | import android.view.View; 20 | 21 | import com.an.customview.R; 22 | 23 | /** 24 | * 自定义进度条控件 25 | */ 26 | public class ProgressBar extends View { 27 | 28 | private int _borderColor; // 边框颜色 29 | private int _borderSize; // 边框粗细 30 | private int _rectColor; // 矩形颜色 31 | private int _textColor; // 文本颜色 32 | private int _textSize; 33 | private int _orientation; // 水平、垂直绘制 34 | 35 | private Paint _paint; // 画笔 36 | private int _width; 37 | private int _height; 38 | private int _value = 50; 39 | private int _maxValue = 100; 40 | private int _minValue = 0; 41 | 42 | public ProgressBar(Context context, AttributeSet attrs, int defStypeAttr) { 43 | super(context, attrs, defStypeAttr); 44 | initView(context, attrs); 45 | } 46 | 47 | public ProgressBar(Context context, AttributeSet attrs) { 48 | super(context, attrs); 49 | initView(context, attrs); 50 | } 51 | 52 | public ProgressBar(Context context) { 53 | super(context); 54 | initView(); 55 | } 56 | 57 | @Override 58 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 59 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 60 | 61 | _width = getMeasuredWidth() - 1; 62 | _height = getMeasuredHeight() - 1; 63 | } 64 | 65 | @Override 66 | protected void onDraw(Canvas canvas) { 67 | drawBorder(canvas); 68 | drawRect(canvas); 69 | drawText(canvas); 70 | 71 | super.onDraw(canvas); 72 | } 73 | 74 | /** 75 | * 设置进度值 76 | * 77 | * @param value 78 | */ 79 | public void setValue(int value) { 80 | _value = value; 81 | postInvalidate(); 82 | } 83 | 84 | /** 85 | * 获取进度值 86 | * 87 | * @return 88 | */ 89 | public int getValue() { 90 | return _value; 91 | } 92 | 93 | /** 94 | * 初始化控件 95 | * 96 | * @param context 97 | * @param attrs 98 | */ 99 | private void initView(Context context, AttributeSet attrs) { 100 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressBar); 101 | if (typedArray != null) { 102 | _borderColor = typedArray.getColor(R.styleable.ProgressBar_border_color_pb, Color.GREEN); 103 | _rectColor = typedArray.getColor(R.styleable.ProgressBar_rect_color_pb, Color.argb(100, 0, 0, 255)); 104 | _borderSize = typedArray.getInt(R.styleable.ProgressBar_border_size_pb, 2); 105 | _textColor = typedArray.getColor(R.styleable.ProgressBar_text_color_pb, Color.RED); 106 | _textSize = typedArray.getInt(R.styleable.ProgressBar_text_size_pb, 40); 107 | _orientation = typedArray.getInt(R.styleable.ProgressBar_orientation_pb, 0); 108 | } 109 | 110 | _paint = new Paint(); 111 | } 112 | 113 | private void initView() { 114 | _borderColor = Color.GREEN; 115 | _borderSize = 2; 116 | _rectColor = Color.argb(100, 0, 0, 255); 117 | _textColor = Color.RED; 118 | _textSize = 40; 119 | _orientation = 0; 120 | _paint = new Paint(); 121 | } 122 | 123 | /** 124 | * 绘制边框 125 | * 126 | * @param canvas 127 | */ 128 | private void drawBorder(Canvas canvas) { 129 | _paint.setColor(_borderColor); 130 | _paint.setStrokeWidth(_borderSize); 131 | 132 | canvas.drawLine(0, 0, _width, 0, _paint); // 绘制上边 133 | canvas.drawLine(0, 0, 0, _height, _paint); // 绘制左边 134 | canvas.drawLine(_width, 0, _width, _height, _paint); // 绘制右边 135 | canvas.drawLine(0, _height, _width, _height, _paint); // 绘制下边 136 | } 137 | 138 | /** 139 | * 绘制矩形 140 | * 141 | * @param canvas 142 | */ 143 | private void drawRect(Canvas canvas) { 144 | _paint.setColor(_rectColor); 145 | 146 | if (_orientation == 0) { 147 | float perSize = _width / ((float) (_maxValue - _minValue)); 148 | int w = (int) (perSize * _value); 149 | canvas.drawRect(0, 0, w, _height, _paint); 150 | } else { 151 | float perSize = _height / ((float) (_maxValue - _minValue)); 152 | int h = (int) (perSize * _value); 153 | canvas.drawRect(0, _height - h, _width, _height, _paint); 154 | } 155 | } 156 | 157 | /** 158 | * 绘制文本 159 | * 160 | * @param canvas 161 | */ 162 | private void drawText(Canvas canvas) { 163 | _paint.setColor(_textColor); 164 | _paint.setTextSize(_textSize); 165 | String text = _value + "%"; 166 | float measureLength = _paint.measureText(text); 167 | 168 | if (_orientation == 0) { 169 | int w = _width / 2 - (int) (measureLength / 2); 170 | canvas.drawText(text, w, _height / 3 * 2, _paint); 171 | } else { 172 | canvas.drawText(text, _width / 2 - (int) (measureLength / 2), _height / 2, _paint); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/RadarView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @Title: RadarView.java 3 | * @Package: com.an.view 4 | * @Description: 自定义雷达控件 5 | * @Author: AnuoF 6 | * @QQ/WeChat: 188512936 7 | * @Date 2019.09.01 11:30 8 | * @Version V1.0 9 | */ 10 | 11 | package com.an.view; 12 | 13 | import android.content.Context; 14 | import android.graphics.Bitmap; 15 | import android.graphics.Canvas; 16 | import android.graphics.Color; 17 | import android.graphics.Paint; 18 | import android.graphics.RadialGradient; 19 | import android.graphics.Rect; 20 | import android.graphics.Shader; 21 | import android.graphics.SweepGradient; 22 | import android.os.Build; 23 | import android.util.AttributeSet; 24 | import android.view.View; 25 | 26 | import androidx.annotation.RequiresApi; 27 | 28 | /** 29 | * 自定义雷达控件 30 | */ 31 | public class RadarView extends View { 32 | 33 | private int _margin; // 边距 34 | 35 | private Bitmap _backBmp; // 背景 36 | private Canvas _backCanvas; 37 | 38 | private int _width; 39 | private int _height; 40 | 41 | private Paint _paint; 42 | private Paint _gradientPaint; 43 | 44 | 45 | public RadarView(Context context, AttributeSet attributeSet, int defStyle) { 46 | super(context, attributeSet, defStyle); 47 | initView(); 48 | } 49 | 50 | public RadarView(Context context, AttributeSet attributeSet) { 51 | super(context, attributeSet); 52 | initView(); 53 | } 54 | 55 | private void initView() { 56 | _paint = new Paint(); 57 | _gradientPaint = new Paint(); 58 | _gradientPaint.setStyle(Paint.Style.FILL); 59 | _margin = 30; 60 | } 61 | 62 | @Override 63 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 64 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 65 | 66 | _width = getMeasuredWidth(); 67 | _height = getMeasuredHeight(); 68 | 69 | if (_backBmp != null) { 70 | _backBmp.recycle(); 71 | _backBmp = null; 72 | } 73 | 74 | _backBmp = Bitmap.createBitmap(_width, _height, Bitmap.Config.ARGB_8888); 75 | _backCanvas = new Canvas(_backBmp); 76 | initBackground(); 77 | } 78 | 79 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 80 | @Override 81 | protected void onDraw(Canvas canvas) { 82 | drawBackground(canvas); 83 | drawPoint(canvas); 84 | drawScan(canvas); 85 | super.onDraw(canvas); 86 | } 87 | 88 | /** 89 | * 初始化背景 90 | */ 91 | private void initBackground() { 92 | int px = _width / 2; 93 | int py = _height / 2; // 圆心 94 | int r = Math.min(px, py) - _margin; // 半径 95 | 96 | _paint.setColor(Color.GREEN); 97 | _paint.setStyle(Paint.Style.STROKE); 98 | 99 | // 画放射线,画刻度文本 100 | for (int i = 0; i < 15; i++) { 101 | _backCanvas.drawLine(px, py, px, py - r, _paint); 102 | String text = i * 15 + ""; 103 | Rect textRect = new Rect(); 104 | _paint.getTextBounds(text, 0, text.length(), textRect); 105 | _backCanvas.drawText(text, px - textRect.width() / 2, py - r - textRect.height(), _paint); 106 | _backCanvas.rotate(24, px, py); // 3.75 = 360 / 96 107 | } 108 | 109 | // 画圆 110 | for (int i = 1; i <= 5; i++) { // 5个圆 111 | int minR = (int) (i * (r / (float) 5)); 112 | _backCanvas.drawCircle(px, py, minR, _paint); 113 | } 114 | } 115 | 116 | /** 117 | * 画背景 118 | * 119 | * @param canvas 120 | */ 121 | private void drawBackground(Canvas canvas) { 122 | if (_backBmp != null) { 123 | canvas.drawBitmap(_backBmp, 0, 0, _paint); 124 | } 125 | } 126 | 127 | private void drawPoint(Canvas canvas) { 128 | int px = _width / 2; 129 | int py = _height / 2; // 圆心 130 | int r = Math.min(px, py) - _margin; // 半径 131 | 132 | RadialGradient radialGradient; 133 | radialGradient = new RadialGradient(px, py - r / 2, r / 20, new int[]{Color.GREEN, Color.argb(10, 0, 255, 0)}, null, Shader.TileMode.CLAMP); 134 | _gradientPaint.setShader(radialGradient); 135 | canvas.drawCircle(px, py - r / 2, r / 20, _gradientPaint); 136 | radialGradient = new RadialGradient(px - r / 2, py, r / 20, new int[]{Color.GREEN, Color.argb(10, 0, 255, 0)}, null, Shader.TileMode.CLAMP); 137 | _gradientPaint.setShader(radialGradient); 138 | canvas.drawCircle(px - r / 2, py, r / 20, _gradientPaint); 139 | radialGradient = new RadialGradient(px - r / 3 * 2, py + r / 2, r / 20, new int[]{Color.GREEN, Color.argb(10, 0, 255, 0)}, null, Shader.TileMode.CLAMP); 140 | _gradientPaint.setShader(radialGradient); 141 | canvas.drawCircle(px - r / 3 * 2, py + r / 2, r / 20, _gradientPaint); 142 | } 143 | 144 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 145 | private void drawScan(Canvas canvas) { 146 | int px = _width / 2; 147 | int py = _height / 2; // 圆心 148 | int r = Math.min(px, py) - _margin; // 半径 149 | 150 | int[] colors = new int[]{Color.GREEN, Color.argb(200, 0, 255, 0), Color.argb(150, 0, 255, 0), Color.argb(100, 0, 255, 0), Color.argb(50, 0, 255, 0), Color.argb(10, 0, 255, 0)}; 151 | SweepGradient sweepGradient = new SweepGradient(px, py, colors, null); 152 | _gradientPaint.setShader(sweepGradient); 153 | canvas.drawArc(px - r, py - r, px + r, py + r, 0, 45, true, _gradientPaint); 154 | 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/ScaleBar.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @Title: ScaleBar.java 3 | * @Package: com.an.view 4 | * @Description: 自定义刻度条控件 5 | * @Author: AnuoF 6 | * @QQ/WeChat: 188512936 7 | * @Date: 2019.08.01 20:00 8 | * @Version: V1.0 9 | */ 10 | 11 | package com.an.view; 12 | 13 | import android.content.Context; 14 | import android.content.res.TypedArray; 15 | import android.graphics.Canvas; 16 | import android.graphics.Color; 17 | import android.graphics.Paint; 18 | import android.util.AttributeSet; 19 | import android.view.View; 20 | 21 | import com.an.customview.R; 22 | 23 | import java.text.ParseException; 24 | import java.text.SimpleDateFormat; 25 | import java.util.Date; 26 | 27 | /** 28 | * 自定义刻度条控件:可用于显示电平、质量等值 29 | */ 30 | public class ScaleBar extends View { 31 | 32 | private Paint _paint; // 画笔 33 | private int _maxValue; // 刻度显示的最大值 34 | private int _minValue; // 刻度显示的最小值 35 | private int _scaleCount; // 刻度几等分 36 | private String _title; // 刻度条显示的标题 37 | private int _titleHeight; // 标题预留的空间 38 | private int _orientation; // 水平,垂直绘制 39 | private int _barColor; // 刻度条颜色 40 | private int _scaleColor; // 刻度线条颜色 41 | 42 | private int _scaleLineLength; // 刻度线的长,这个自适应 43 | private int _value; // 刻度值 44 | private int _width; // View 宽度值 45 | private int _height; // View 高度值 46 | 47 | public ScaleBar(Context context, AttributeSet attrs, int defStypeAttr) { 48 | super(context, attrs, defStypeAttr); 49 | init(context, attrs); 50 | } 51 | 52 | public ScaleBar(Context context, AttributeSet attrs) { 53 | super(context, attrs); 54 | init(context, attrs); 55 | } 56 | 57 | public ScaleBar(Context context) { 58 | super(context); 59 | init(); 60 | } 61 | 62 | /** 63 | * 获取刻度值 64 | * 65 | * @param value 66 | */ 67 | public void setValue(int value) { 68 | if (value < _minValue) { 69 | _value = _minValue; 70 | } else if (value > _maxValue) { 71 | _value = _maxValue; 72 | } else { 73 | _value = value; 74 | } 75 | postInvalidate(); 76 | } 77 | 78 | /** 79 | * 设置刻度值 80 | * 81 | * @return 82 | */ 83 | public int getValue() { 84 | return _value; 85 | } 86 | 87 | /** 88 | * 初始化 89 | * 90 | * @param context 91 | * @param attrs 92 | */ 93 | private void init(Context context, AttributeSet attrs) { 94 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ScaleBar); 95 | if (typedArray != null) { 96 | _scaleColor = typedArray.getColor(R.styleable.ScaleBar_scale_color_sb, Color.GREEN); 97 | _paint = new Paint(); 98 | 99 | _maxValue = typedArray.getInt(R.styleable.ScaleBar_max_value_sb, 100); 100 | _minValue = typedArray.getInt(R.styleable.ScaleBar_min_value_sb, 0); 101 | _scaleCount = typedArray.getInt(R.styleable.ScaleBar_scale_count_sb, 10); 102 | _title = typedArray.getString(R.styleable.ScaleBar_title_sb); 103 | _titleHeight = typedArray.getInt(R.styleable.ScaleBar_title_height_sb, 80); 104 | _orientation = typedArray.getInt(R.styleable.ScaleBar_orientation_sb, 0); 105 | _barColor = typedArray.getColor(R.styleable.ScaleBar_bar_color_sb, Color.GREEN); 106 | _value = _minValue; 107 | } else { 108 | init(); 109 | } 110 | } 111 | 112 | /** 113 | * 初始化 114 | */ 115 | private void init() { 116 | _paint = new Paint(); 117 | _paint.setColor(Color.GREEN); 118 | _maxValue = 100; 119 | _minValue = 0; 120 | _scaleCount = 10; 121 | _title = ""; 122 | _orientation = 0; 123 | _value = _minValue; 124 | } 125 | 126 | @Override 127 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 128 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 129 | _width = getMeasuredWidth() - 1; // 宽度值 130 | _height = getMeasuredHeight() - 1; // 高度值 131 | 132 | if (_orientation == 0) { 133 | _scaleLineLength = _height / 4; 134 | } else { 135 | _scaleLineLength = _width / 4; 136 | } 137 | } 138 | 139 | @Override 140 | protected void onDraw(Canvas canvas) { 141 | _paint.setColor(_scaleColor); 142 | drawBorker(canvas); 143 | boolean valid = Utils.checkValid(); 144 | if (valid) { 145 | if (_orientation == 0) { 146 | _paint.setTextSize(_height / 3); 147 | } else { 148 | _paint.setTextSize(_width / 3); // 设置文字大小 149 | } 150 | drawTitle(canvas); 151 | drawScale(canvas); 152 | canvas.save(); 153 | drawValueBar(canvas); 154 | } else { 155 | drawExpired(canvas); 156 | } 157 | 158 | super.onDraw(canvas); 159 | } 160 | 161 | /** 162 | * 绘制边框 163 | * 164 | * @param canvas 165 | */ 166 | private void drawBorker(Canvas canvas) { 167 | canvas.drawLine(0, 0, _width, 0, _paint); // 画顶边 168 | canvas.drawLine(0, 0, 0, _height, _paint); // 画左边 169 | canvas.drawLine(_width, 0, _width, _height, _paint); // 画右边 170 | canvas.drawLine(0, _height, _width, _height, _paint); // 画底边 171 | } 172 | 173 | /** 174 | * 绘制无效提示 175 | * 176 | * @param canvas 177 | */ 178 | private void drawExpired(Canvas canvas) { 179 | // Control expired, please contact the author 180 | _paint.setColor(Color.RED); 181 | if (_orientation == 0) { 182 | // 水平布局 183 | canvas.drawText("Control expired, please contact the author", 0, _height / 2, _paint); 184 | } else { 185 | // 垂直布局 186 | canvas.drawText("Control", _width / 2, 50, _paint); 187 | canvas.drawText("expired", _width / 2, 90, _paint); 188 | canvas.drawText("please", _width / 2, 130, _paint); 189 | canvas.drawText("contact", _width / 2, 170, _paint); 190 | canvas.drawText("the", _width / 2, 210, _paint); 191 | canvas.drawText("author", _width / 2, 250, _paint); 192 | } 193 | } 194 | 195 | /** 196 | * 画标题 197 | * 198 | * @param canvas 199 | */ 200 | private void drawTitle(Canvas canvas) { 201 | if (_orientation == 0) { 202 | if (_title == null || _title.length() <= 0) { 203 | _titleHeight = 30; 204 | } else { 205 | canvas.drawText(_title, _width - _titleHeight / 2 - _paint.measureText(_title) / 2, _height / 2, _paint); 206 | } 207 | } else { 208 | if (_title == null || _title.length() <= 0) { 209 | _titleHeight = 30; 210 | } else { 211 | canvas.drawText(_title, _width / 2 - _paint.measureText(_title) / 2, _titleHeight / 2, _paint); // 居中 212 | } 213 | } 214 | } 215 | 216 | /** 217 | * 画刻度 218 | * 219 | * @param canvas 220 | */ 221 | private void drawScale(Canvas canvas) { 222 | if (_orientation == 0) { 223 | float oneScaleWidth = ((float) (_width - _titleHeight)) / _scaleCount; 224 | for (int i = 0; i <= _scaleCount; i++) { 225 | float width = i * oneScaleWidth; 226 | canvas.drawLine(width, _height, width, _height - _scaleLineLength, _paint); 227 | canvas.drawText((_minValue + ((_maxValue - _minValue) / _scaleCount) * i) + "", width, _height - _scaleLineLength, _paint); // 228 | } 229 | } else { 230 | float oneScaleHeight = ((float) (_height - _titleHeight)) / _scaleCount; 231 | for (int i = 0; i <= _scaleCount; i++) { 232 | float height = i * oneScaleHeight + _titleHeight; 233 | canvas.drawLine(0, height, _scaleLineLength, height, _paint); 234 | //canvas.drawText((_maxValue - _minValue) / _scaleCount * (_scaleCount - i) + "", _scaleLineLength, height, _paint); 235 | canvas.drawText(_maxValue - (_maxValue - _minValue) / _scaleCount * i + "", _scaleLineLength, height, _paint); 236 | } 237 | } 238 | } 239 | 240 | /** 241 | * 画刻度值条 242 | * 243 | * @param canvas 244 | */ 245 | private void drawValueBar(Canvas canvas) { 246 | _paint.setColor(_barColor); 247 | 248 | if (_orientation == 0) { 249 | canvas.drawRect(0, 0, (_value - _minValue) / ((_maxValue - _minValue) / (float) _scaleCount) * ((_width - _titleHeight) / (float) _scaleCount), _height, _paint); 250 | } else { 251 | canvas.drawRect(0, _height - ((_value - _minValue) / ((_maxValue - _minValue) / (float) _scaleCount)) * ((_height - _titleHeight) / (float) _scaleCount), _width, _height, _paint); 252 | } 253 | } 254 | 255 | } 256 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/ShowMode.java: -------------------------------------------------------------------------------- 1 | package com.an.view; 2 | 3 | /** 4 | * 显示模式枚举 5 | */ 6 | public enum ShowMode { 7 | Both, // 频谱图和瀑布图都显示 8 | Spectrum, // 只显示频谱图 9 | Waterfall // 只显示瀑布图 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/Utils.java: -------------------------------------------------------------------------------- 1 | package com.an.view; 2 | 3 | import android.graphics.Color; 4 | import android.os.Build; 5 | 6 | import androidx.annotation.RequiresApi; 7 | 8 | import java.text.ParseException; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | 12 | public class Utils { 13 | 14 | public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 15 | 16 | /** 17 | * 判断点是否在矩形内(在边上也算) 18 | * 19 | * @param startX 20 | * @param startY 21 | * @param endX 22 | * @param endY 23 | * @param x 24 | * @param y 25 | * @return 26 | */ 27 | public static boolean IsPointInRect(int startX, int startY, int endX, int endY, int x, int y) { 28 | if (x >= startX && x <= endX && y >= startY && y <= endY) { 29 | return true; 30 | } else { 31 | return false; 32 | } 33 | } 34 | 35 | /** 36 | * 检查是否过期 37 | * 38 | * @return 39 | */ 40 | public static boolean checkValid() { 41 | long currentTime = System.currentTimeMillis(); 42 | String timeNow = Utils.sdf.format(currentTime); 43 | boolean outofdate = false; 44 | try { 45 | Date lisenseDate = Utils.sdf.parse("2020-04-1 00:00:00"); // April Fools' Day 46 | Date currDate = Utils.sdf.parse(timeNow); 47 | if (currDate.compareTo(lisenseDate) > 0) { 48 | outofdate = true; 49 | } 50 | 51 | return !outofdate; 52 | } catch (ParseException e) { 53 | e.printStackTrace(); 54 | return true; 55 | } 56 | } 57 | 58 | /** 59 | * Color 到十六进制的转换 60 | * 61 | * @param color 62 | * @return 63 | */ 64 | @RequiresApi(api = Build.VERSION_CODES.O) 65 | public static String colorToHexValue(Color color) { 66 | return intToHexValue((int) color.alpha()) + intToHexValue((int) color.red()) + intToHexValue((int) color.green()) + intToHexValue((int) color.blue()); 67 | } 68 | 69 | /** 70 | * int 到十六进制的转换 71 | * 72 | * @param number 73 | * @return 74 | */ 75 | public static String intToHexValue(int number) { 76 | String result = Integer.toHexString(number & 0xff); 77 | while (result.length() < 2) { 78 | result = "0" + result; 79 | } 80 | return result.toUpperCase(); 81 | } 82 | 83 | /** 84 | * 十六进制到 Color 的转换 85 | * 86 | * @param str 87 | * @return 88 | */ 89 | public static int fromStrToARGB(String str) { 90 | String str1 = str.substring(0, 2); 91 | String str2 = str.substring(2, 4); 92 | String str3 = str.substring(4, 6); 93 | String str4 = str.substring(6, 8); 94 | int alpha = Integer.parseInt(str1, 16); 95 | int red = Integer.parseInt(str2, 16); 96 | int green = Integer.parseInt(str3, 16); 97 | int blue = Integer.parseInt(str4, 16); 98 | return Color.argb(alpha, red, green, blue); // new Color(red, green, blue, alpha); 99 | } 100 | 101 | public static int fromStrToRGB(String str) { 102 | String redStr = str.substring(0, 2); 103 | String greenStr = str.substring(2, 4); 104 | String blueStr = str.substring(4, 6); 105 | return Color.rgb(Integer.parseInt(redStr), Integer.parseInt(greenStr), Integer.parseInt(blueStr)); 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /app/src/main/java/com/an/view/WaterfallCanvas.java: -------------------------------------------------------------------------------- 1 | package com.an.view; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | class WaterfallCanvas { 12 | 13 | public int _rainRow; // 雨点图行数,Y轴数据量 14 | public short _ZAxisMax; // Z轴最大值 15 | public short _ZAxisMin; // Z轴最小智 16 | public int _backgroundColor; // 背景颜色 17 | public int[] _colors; // 色带 18 | // 以上变量由外部赋值 19 | 20 | private List _data; // 数据映射到颜色的二维数组 21 | private double _frequency; 22 | private double _spectrumSpan; 23 | private int _pointCount; 24 | 25 | private Canvas _canvas; 26 | private WaterfallView _waterFallView; 27 | private int _width; 28 | private int _height; 29 | private Paint _rainPaint; 30 | 31 | private boolean _indexChanged; 32 | private int _startIndex; 33 | private int _endIndex; 34 | 35 | private boolean _bRool; 36 | private final Object _lockObj = new Object(); 37 | private OnDrawFinishedListener _callback; 38 | 39 | 40 | public WaterfallCanvas(Canvas canvas, WaterfallView waterfallView) { 41 | _canvas = canvas; 42 | _waterFallView = waterfallView; 43 | _width = _canvas.getWidth(); 44 | _height = _canvas.getHeight(); 45 | 46 | _rainPaint = new Paint(); 47 | _rainPaint.setStyle(Paint.Style.FILL); 48 | _data = new ArrayList<>(); 49 | _rainRow = 500; 50 | _ZAxisMax = 80; 51 | _ZAxisMin = -20; 52 | _bRool = true; 53 | 54 | _backgroundColor = Color.BLACK; 55 | _startIndex = 0; 56 | _indexChanged = false; 57 | _callback = (OnDrawFinishedListener) waterfallView; 58 | } 59 | 60 | public void setData(double frequency, double span, float[] data) { 61 | synchronized (_lockObj) { 62 | if (_endIndex == 0) { 63 | _startIndex = 0; 64 | _endIndex = data.length; 65 | } 66 | 67 | if (_data.size() == 0) { 68 | _frequency = frequency; 69 | _spectrumSpan = span; 70 | _pointCount = data.length; 71 | } else if (frequency != _frequency || span != _spectrumSpan || data.length != _pointCount) { 72 | clear(); 73 | _frequency = frequency; 74 | _spectrumSpan = span; 75 | _pointCount = data.length; 76 | } 77 | 78 | byte[] colors = new byte[data.length]; 79 | for (int i = 0; i < data.length; i++) { 80 | byte index; 81 | float value = data[i]; 82 | if (value <= _ZAxisMin) { 83 | index = (byte) (_bRool ? 1 : 0); 84 | } else if (value >= _ZAxisMax) { 85 | index = (byte) (_colors.length - 1); 86 | } else { 87 | index = (byte) ((value - _ZAxisMin) / (_ZAxisMax - _ZAxisMin) * (_colors.length - 1)); 88 | } 89 | 90 | colors[i] = index; 91 | } 92 | 93 | if (_data.size() >= _rainRow) { 94 | _data.remove(0); 95 | } 96 | _data.add(colors); 97 | 98 | drawWaterfall(); 99 | } 100 | 101 | _callback.onDrawFinished(); 102 | } 103 | 104 | public void zoneRange(int startIndex, int endIndex) { 105 | if (_startIndex == startIndex && _endIndex == endIndex) 106 | return; 107 | 108 | synchronized (_lockObj) { 109 | _startIndex = startIndex; 110 | _endIndex = endIndex; 111 | _indexChanged = true; 112 | 113 | drawWaterfall(); 114 | } 115 | 116 | _callback.onDrawFinished(); 117 | } 118 | 119 | public void clear() { 120 | synchronized (_lockObj) { 121 | _startIndex = 0; 122 | _endIndex = 0; 123 | _data.clear(); 124 | 125 | drawWaterfall(); 126 | } 127 | 128 | _callback.onDrawFinished(); 129 | } 130 | 131 | /** 132 | * 绘制瀑布图。为保证效率,不需要每次全部重绘,而是绘制新增的数据即可。 133 | */ 134 | private void drawWaterfall() { 135 | if (_data == null || _data.size() == 0) 136 | return; 137 | if (_waterFallView == null || _waterFallView._bitmap == null) 138 | return; 139 | 140 | float perWidth = (_width) / (float) (_endIndex - _startIndex); // 每个方格的 宽 141 | float perHeight = (_height) / (float) _rainRow; // 每个方格的 高 142 | 143 | if (_data.size() == 1) { 144 | for (int h = _startIndex; h < _endIndex; h++) { 145 | int width = (int) ((h - _startIndex) * perWidth); 146 | int height = 0; 147 | _rainPaint.setColor(_colors[_colors.length - 1 - _data.get(0)[h]]); 148 | _canvas.drawRect(width, height, width + perWidth, height + perHeight, _rainPaint); 149 | } 150 | }else { 151 | // 先绘制之前的 Bitmap,然后再画新的数据 152 | Bitmap bitmap = Bitmap.createBitmap(_waterFallView._bitmap, 0, (int) perHeight, _width, (int) ((_data.size() - 1) * perHeight)); // perHeight 必须 >= 1,也就是 _rainRow 必须 <= _height 153 | _canvas.drawBitmap(bitmap, 0, 0, _rainPaint); 154 | bitmap.recycle(); 155 | 156 | for (int h = _startIndex; h < _endIndex; h++) { 157 | if (h >= _endIndex) 158 | break; 159 | 160 | int width = (int) ((h - _startIndex) * perWidth); 161 | int height = (int) ((_data.size() - 1) * perHeight); 162 | _rainPaint.setColor(_colors[_colors.length - 1 - _data.get(_data.size() - 1)[h]]); // 只画最后一包 163 | _canvas.drawRect(width, height, width + perWidth, height + perHeight, _rainPaint); 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /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/car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/drawable/car.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/df.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/drawable/df.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_circle_process_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_compass_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 23 | 24 | 33 | 34 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_df.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_general_spectrum_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 21 | 22 | 23 | 27 | 28 | 33 | 34 | 40 | 41 | 46 | 47 | 52 | 53 | 54 | 55 | 58 | 59 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_level_stream_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 22 | 30 | 31 | 35 | 36 |