├── .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 |
6 |
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 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 安卓自定义控件,有电平流、频谱图、瀑布图、刻度条、指南针、进度条,待添加....
3 |
4 | ## LevelStremView ##
5 |
6 | 电平流控件,样式如下:
7 |
8 | 
9 |
10 | 
11 |
12 | ## SpectrumView WaterfallView ##
13 |
14 | 频谱图控件,样式如下:
15 |
16 | 
17 |
18 | 瀑布图控件,样色如下:
19 |
20 | 
21 |
22 | 通用频谱图控件(频谱图与瀑布图的组合)
23 |
24 | 
25 |
26 | ## ScaleBar ##
27 |
28 | 刻度条控件,样式如下:
29 |
30 | 
31 |
32 | ## CompassView ##
33 |
34 | 指南针控件,样式如下:
35 |
36 | 
37 |
38 | ## ProgressBar ##
39 |
40 | 进度条控件,样式如下:
41 |
42 | 
--------------------------------------------------------------------------------
/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 |
41 |
42 |
47 |
48 |
53 |
54 |
59 |
60 |
65 |
66 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
14 |
20 |
21 |
27 |
28 |
34 |
35 |
41 |
42 |
43 |
44 |
47 |
48 |
54 |
55 |
61 |
62 |
68 |
69 |
75 |
76 |
77 |
80 |
81 |
87 |
88 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_progress_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
28 |
29 |
35 |
36 |
51 |
52 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_radar_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_scale_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
25 |
26 |
35 |
36 |
48 |
49 |
53 |
65 |
66 |
79 |
80 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_spectrum_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
31 |
32 |
35 |
36 |
41 |
42 |
47 |
48 |
53 |
54 |
59 |
60 |
65 |
66 |
71 |
72 |
73 |
74 |
77 |
78 |
84 |
85 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_waterfull_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_gradient_color_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
16 |
17 |
23 |
24 |
29 |
30 |
33 |
34 |
35 |
41 |
42 |
47 |
48 |
51 |
52 |
53 |
59 |
60 |
65 |
66 |
69 |
70 |
71 |
77 |
78 |
83 |
84 |
87 |
88 |
89 |
90 |
91 |
92 |
96 |
97 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/an_48px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-xhdpi/an_48px.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 自定义控件
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/test/java/com/an/customview/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.an.customview;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | jcenter()
7 |
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.5.0'
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 |
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/doc/安卓自定义瀑布图控件.md:
--------------------------------------------------------------------------------
1 | # 安卓自定义瀑布图控件 #
2 |
3 | ## 引言 ##
4 |
5 | 之前写过电平图和频谱图的实现的文章,在这片文章中,我将要讲解频谱瀑布图的实现。
6 |
7 | 频谱瀑布图又叫谱阵图,它是将振动信号的功率谱或幅值谱随转速变化而叠置而成的三维谱图,显示振动信号中各谐波成分随转速变化的情况。普通频谱图x轴是频率,y轴是幅度;而瀑布图x轴是频率,y轴是时间,幅度则用不同颜色表示,随着时间的的变化,整个频谱由上到下移动,看起来像瀑布,所以叫瀑布图。
8 |
9 | 瀑布图也叫雨点图,对于查看猝发信号或者跳频信号的效果十分明显,可以很清晰的看到信号的变化,这里就不模拟和展示了。
10 |
11 | 首先看下效果图:
12 |
13 | 
14 |
15 | 
16 |
17 | 
18 |
19 | 
20 |
21 | 实现了,色带自定义,频谱信号缩放,手指按下显示当前点的频率等。
22 |
23 | ## 实现 ##
24 |
25 | ### 布局 ###
26 |
27 | 这个的布局相对比较简单,就是一个色带区和信号绘制区,如下图:
28 |
29 | 
30 |
31 | 左边是一个渐变的色带,其对应右边信号幅度的强弱。
32 |
33 | ### 编码 ###
34 |
35 | 自定义控件,前面的步骤都差不多,新建类,attrs.xml,初始化,onMeasure(),onDraw(),我们直接从onDraw()开始吧。
36 |
37 | 在我最初实现第一个版本的时候,性能问题非常严重,绘制的点数多了之后,界面卡顿非常严重,后来我在绘图的机制和绘图的效率方面做了优化之后,效果非常好,测试Y轴1000行,X轴801个点,非常流畅,毫无压力。
38 |
39 | 我的第一个优化点是,采用离屏Canvas机制:即异步方式绘制图形,完成之后再回调通知onDraw()方法进行屏幕绘制。
40 |
41 | 第二个优化点是,在对数据的处理时,不再保存矩阵数据,不做横向和纵向的矩阵遍历。已绘制了的信号直接裁剪Bitmap,新到的信号才遍历绘制,然后贴在一起,就完成信号的绘制。这就完成性能质的飞跃啊。
42 |
43 | 那我们就看代码吧
44 |
45 | 在onDraw()中,我们首先绘制左边色带drawGradientRect():
46 |
47 | /**
48 | * 画色带
49 | *
50 | * @param canvas
51 | */
52 | private void drawGradientRect(Canvas canvas) {
53 | LinearGradient linearGradient = new LinearGradient(0, 0, _marginLeft, _height, _colors, null, Shader.TileMode.CLAMP);
54 | _paint.setShader(linearGradient);
55 | canvas.drawRect(0, 0, _marginLeft, _height, _paint);
56 | }
57 |
58 | 效果图如下:
59 |
60 | 
61 |
62 | 然后就在setData()等待频谱数据,数据来了之后就启动线程池执行绘制:
63 |
64 | public void setData(final double frequency, final double span, final float[] data) {
65 | if (frequency != _frequency || span != _spectrumSpan) {
66 | _startIndex = 0;
67 | _endIndex = data.length;
68 | _frequency = frequency;
69 | _spectrumSpan = span;
70 | _dataLength = data.length;
71 | }
72 |
73 | if (_wCanvas != null) {
74 | _executorService.execute(new Runnable() {
75 | @Override
76 | public void run() {
77 | if (_wCanvas != null) {
78 | _wCanvas.setData(frequency, span, data);
79 | }
80 | }
81 | });
82 | }
83 | }
84 |
85 | 在启动线程绘制的过程中,首先是根据频谱的幅度值,映射到色带中的颜色值(怎么映射,请看代码),然后才是绘制drawWaterfall()
86 |
87 | /**
88 | * 绘制瀑布图。为保证效率,不需要每次全部重绘,而是绘制新增的数据即可。
89 | */
90 | private void drawWaterfall() {
91 | if (_data == null || _data.size() == 0)
92 | return;
93 | if (_waterFallView == null || _waterFallView._bitmap == null)
94 | return;
95 |
96 | float perWidth = (_width) / (float) (_endIndex - _startIndex); // 每个方格的 宽
97 | float perHeight = (_height) / (float) _rainRow; // 每个方格的 高
98 |
99 | if (_data.size() == 1) {
100 | for (int h = _startIndex; h < _endIndex; h++) {
101 | int width = (int) ((h - _startIndex) * perWidth);
102 | int height = 0;
103 | _rainPaint.setColor(_colors[_colors.length - 1 - _data.get(0)[h]]);
104 | _canvas.drawRect(width, height, width + perWidth, height + perHeight, _rainPaint);
105 | }
106 | }else {
107 | // 先绘制之前的 Bitmap,然后再画新的数据
108 | Bitmap bitmap = Bitmap.createBitmap(_waterFallView._bitmap, 0, (int) perHeight, _width, (int) ((_data.size() - 1) * perHeight)); // perHeight 必须 >= 1,也就是 _rainRow 必须 <= _height
109 | _canvas.drawBitmap(bitmap, 0, 0, _rainPaint);
110 | bitmap.recycle();
111 |
112 | for (int h = _startIndex; h < _endIndex; h++) {
113 | if (h >= _endIndex)
114 | break;
115 |
116 | int width = (int) ((h - _startIndex) * perWidth);
117 | int height = (int) ((_data.size() - 1) * perHeight);
118 | _rainPaint.setColor(_colors[_colors.length - 1 - _data.get(_data.size() - 1)[h]]); // 只画最后一包
119 | _canvas.drawRect(width, height, width + perWidth, height + perHeight, _rainPaint);
120 | }
121 | }
122 | }
123 |
124 | 上面的方法就是我在之前说的第二个优化点。在绘制完成之后,再调用回调通知onDraw()绘制图形到屏幕:
125 |
126 | _callback.onDrawFinished();
127 |
128 | @Override
129 | protected void onDraw(final Canvas canvas) {
130 | drawGradientRect(canvas);
131 | if (_bitmap != null) {
132 | canvas.drawBitmap(_bitmap, _marginLeft, _marginTop, _paint);
133 | }
134 | drawSelectRect(canvas);
135 |
136 | super.onDraw(canvas);
137 | }
138 |
139 | 最后是信号的放大和缩小,这个的实现和频谱图的实现基本一致,即根据手指的位置来确定_startIndex和_endIndex,然后取相应的数据进行绘制即可。
140 |
141 | 好了,就到这里吧,文章只说个大概,详情请看代码,或者联系我:
142 |
143 | /**
144 | * @Title: WaterfallView.java
145 | * @Package: com.an.view
146 | * @Description: 自定义频谱瀑布图控件
147 | * @Author: AnuoF
148 | * @QQ/WeChat: 188512936
149 | * @Date 2019.08.14 11:50
150 | * @Version V1.0
151 | */
152 |
153 | 最后,奉上源码,自由、开源:[https://github.com/AnuoF/android_customview](https://github.com/AnuoF/android_customview)
154 |
155 | AnuoF
156 | Chengdu
157 | Aug 20,2019
--------------------------------------------------------------------------------
/doc/安卓自定义电平流控件.md:
--------------------------------------------------------------------------------
1 | # 安卓自定义电平流图形控件 #
2 |
3 | ## 引言 ##
4 |
5 | 在无线电监测方面,需要对信号进行展示,其中一项数据就是设备返回的电平数据,需要对其实时展示,一图胜千言,最好且最直观的方式就是图表展示,这样对其信号强弱的变化,就可以一目了然。
6 |
7 | 本文主要讲安卓版的电平流图形控件的实现,首先效果图如下:
8 |
9 | 
10 |
11 | 实现了Y轴的拖动、放大、缩小,以及自动调整比例显示。
12 |
13 |
14 | ## 实现 ##
15 |
16 | ### 布局 ###
17 |
18 | 自定义控件,首先是要设计布局,要绘制哪些元素,元素的位置,弄清楚之后,就可以开始动手画了,我们最终的电平图如下:
19 |
20 | 
21 |
22 | 那么我们先设计布局,如下如所示:
23 |
24 | 
25 |
26 | 首先我们在左边要绘制单位,然后是刻度区,然后在是电平折线区。另外考虑到留边,所以在上下左右都加入了边距,便于动态调整。
27 |
28 | ### 编码 ###
29 |
30 | 图纸(布局)确定之后,那么接下来就是搬砖(编码)了,首先新建一个类LevelStreamView继承View,然后实现必要的构造函数,如下:
31 |
32 | public LevelStreamView(Context context, AttributeSet attrs, int defStypeAttr) {
33 | super(context, attrs, defStypeAttr);
34 | initView(context, attrs);
35 | }
36 |
37 | public LevelStreamView(Context context, AttributeSet attrs) {
38 | super(context, attrs);
39 | initView(context, attrs);
40 | }
41 |
42 | public LevelStreamView(Context context) {
43 | super(context);
44 | initView();
45 | }
46 |
47 | 然后在attrs.xml中自定义控件的一些属性,方便调用者动态更改某些属性,代码如下:
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | 然后在构造函数中调用initView()初始化函数,读取刚才在attr.xml中设置的控件属性。
83 |
84 | 接下来就是具体的图形绘制了,在绘制之前,先重载onMeasure()和onDraw()方法,在onMeasure()方法中确定控件的宽和高:
85 |
86 | _width = getMeasuredWidth();
87 | _height = getMeasuredHeight();
88 |
89 | 然后在onDraw()里面就是具体的画图了,首先是画单位drawUnit(),代码如下:
90 |
91 | private void drawUnit(Canvas canvas) {
92 | if (_unitStr == null || _unitStr.length() == 0)
93 | return;
94 |
95 | canvas.rotate(-90);
96 | canvas.translate(-_height, 0);
97 |
98 | _paint.setColor(_unitColor);
99 | _paint.setTextSize(_unitSize);
100 | Rect unitRect = new Rect();
101 | _paint.getTextBounds(_unitStr, 0, _unitStr.length(), unitRect);
102 | canvas.drawText(_unitStr, _height / 2 - (unitRect.width() / 2), unitRect.height(), _paint);
103 | canvas.save();
104 | canvas.translate(_height, 0);
105 | canvas.rotate(90);
106 | }
107 |
108 | 画布本来的方向是右下的,先旋转-90°,变成右上的方向,然后再把原点往上移_height,接着就是画文本了,画完之后再把canvas改回原来的样子。
109 |
110 | 接下来就是画刻度和网格了,drawAxis(),先附上代码,然后再进行粗略的讲解。
111 |
112 | private void drawAxis(Canvas canvas) {
113 | _paint.setColor(_gridColor);
114 | _paint.setTextSize(_scale_size);
115 |
116 | int scaleHeight = _height - _marginTop - _marginBottom; // 绘制去总高度
117 | int scaleWidth = _width - _marginLeft - _marginRight - _scaleLineLength; // 绘制去总宽度
118 |
119 | float perScaleHeight = scaleHeight / (float) _gridCount; // 每一格的高度
120 | float perScaleWidth = scaleWidth / (float) _gridCount; // 每一格的宽度
121 | int maxValue = _maxValue + _offsetY - _zoomOffsetY;
122 | int minValue = _minValue + _offsetY + _zoomOffsetY;
123 |
124 | Rect scaleRect = new Rect();
125 | _paint.getTextBounds(maxValue + "", 0, (maxValue + "").length(), scaleRect);
126 |
127 | for (int i = 0; i <= _gridCount; i++) {
128 | int height = (int) (i * perScaleHeight) + _marginTop;
129 | int width = _marginLeft + _scaleLineLength + (int) (i * perScaleWidth);
130 | int textHeight = 0;
131 | int startWidth = _marginLeft;
132 |
133 | if ((i + 1) % 2 != 0) {
134 | if (i == 0) {
135 | textHeight += scaleRect.height();
136 | } else if (i == _gridCount) {
137 |
138 | } else {
139 | textHeight += scaleRect.height() / 2;
140 | }
141 |
142 | int scaleValue = maxValue - i * (maxValue - minValue) / _gridCount;
143 | float scaleTextLen = _paint.measureText(scaleValue + "");
144 | canvas.drawText(scaleValue + "", startWidth - scaleTextLen, height + textHeight, _paint);
145 | } else {
146 | startWidth += _scaleLineLength;
147 | }
148 |
149 | // 画横轴
150 | canvas.drawLine(startWidth, height, _width - _marginRight, height, _paint);
151 | // 画纵轴
152 | canvas.drawLine(width, _marginTop, width, _height - _marginBottom, _paint);
153 | }
154 | }
155 |
156 | 首先设置画笔的颜色和字体大小,然后根据宽度、高度和上下左右边距,以及设置的网格数,计算出每一格的宽度和高度,因为这个参数在画坐标轴和网格时需要用到。
157 |
158 | 然后一个循环,对坐标轴,文本和网格线进行绘制。OK,基本的背景图形就有了,后面再把数据接入即可。
159 |
160 | 
161 |
162 | 绘制电平数据,关键点在于根据电平值和Y坐标显示的值,计算出其应在的位置点的(x,y)。
163 |
164 | private void drawLevel(Canvas canvas) {
165 | if (_levels.size() == 0)
166 | return;
167 |
168 | _paint.setColor(_levelColor);
169 | _paint.setStyle(Paint.Style.STROKE.STROKE);
170 | Path path = new Path();
171 |
172 | int maxValue = _maxValue + _offsetY - _zoomOffsetY;
173 | int minValue = _minValue + _offsetY + _zoomOffsetY;
174 | float perHeight = (_height - _marginTop - _marginBottom) / (float) Math.abs(maxValue - minValue);
175 | float perWidth = (_width - _marginLeft - _scaleLineLength - _marginRight) / (float) _levelCount;
176 |
177 | for (int i = 0; i < _levels.size(); i++) {
178 | float level = _levels.get(i);
179 | int x = _marginLeft + _scaleLineLength + (int) (i * perWidth);
180 | int y = _marginTop + (int) ((maxValue - level) * perHeight);
181 |
182 | if (i == 0) {
183 | path.moveTo(x, y);
184 | } else {
185 | path.lineTo(x, y);
186 | }
187 | }
188 |
189 | canvas.drawPath(path, _paint);
190 |
191 | if (_levels.size() < _levelCount) {
192 | // 如果电平点数还未满时,绘制当前电平电
193 | int index = _levels.size() - 1;
194 | float level = _levels.get(index);
195 | int x = _marginLeft + _scaleLineLength + (int) (index * perWidth);
196 | int y = _marginTop + (int) ((maxValue - level) * perHeight);
197 |
198 | _paint.setStyle(Paint.Style.FILL_AND_STROKE);
199 | canvas.drawCircle(x, y, 5, _paint);
200 | }
201 |
202 | // 覆盖上边和下边,使得绘制的图形看上去只在网格中绘制
203 | _paint.setStyle(Paint.Style.FILL);
204 | Drawable background = getBackground();
205 | if (background instanceof ColorDrawable) {
206 | ColorDrawable colorDrawable = (ColorDrawable) background;
207 | int color = colorDrawable.getColor();
208 | _paint.setColor(color);
209 | canvas.drawRect(_marginLeft + _scaleLineLength, 0, _width - _marginRight, _marginTop, _paint);
210 | canvas.drawRect(_marginLeft + _scaleLineLength, _height - _marginBottom + 1, _width - _marginRight, _height, _paint);
211 | }
212 | }
213 |
214 | 绘制完折线之后,再在上边和下边各绘制一个与背景色相同的矩形,这样看上去电平线就只在网格中绘制。
215 |
216 | 至此,基本的图形就已绘制完成。那么电平流式绘制是怎么实现的呢?很简单,设置最大显示的电平点数,如果电平点数超过了最大值,则移除最开始的那个点,然后在绘制当前电平点数,看起来就是流动的了。
217 |
218 | /**
219 | * 设置电平值
220 | *
221 | * @param level
222 | */
223 | public void setLevel(float level) {
224 | if (_levels.size() > _levelCount) {
225 | _levels.remove(0); // 移除最前一个
226 | }
227 | _levels.add(level);
228 | postInvalidate();
229 | }
230 |
231 | 最后要实现的就是图形的一些手势操作,Y轴拖动,多点触控缩放等。首先要重载onTouch()方法:
232 |
233 | @Override
234 | public boolean onTouch(View view, MotionEvent motionEvent) {
235 | switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
236 | case MotionEvent.ACTION_DOWN:
237 | // 单点触控
238 | actionDown(motionEvent);
239 | break;
240 | case MotionEvent.ACTION_POINTER_DOWN:
241 | // & MotionEvent.ACTION_MASK 才能得到 ACTION_POINTER_DOWN
242 | // 多点触控
243 | actionPointerDown(motionEvent);
244 | break;
245 | case MotionEvent.ACTION_MOVE:
246 | // 移动:缩放或拖动
247 | actionMove(motionEvent);
248 | break;
249 | case MotionEvent.ACTION_POINTER_UP:
250 | // 多点触控抬起
251 | actionPointerUp(motionEvent);
252 | break;
253 | case MotionEvent.ACTION_CANCEL:
254 | case MotionEvent.ACTION_UP:
255 | // 取消或抬起
256 | actionCancelUp(motionEvent);
257 | break;
258 | }
259 |
260 | return true;
261 | }
262 |
263 | 首先是单点触控,在ACTION_DOWN(即手指按下的事件)中,判断按下的点是否在Y轴刻度区。
264 |
265 | // 以下为手势拖动和缩放实现
266 | private void actionDown(MotionEvent motionEvent) {
267 | // 判断点是否在纵轴刻度上
268 | if (Utils.IsPointInRect(0, 0, _marginLeft + _scaleLineLength, _height, (int) motionEvent.getX(), (int) motionEvent.getY())) {
269 | _handleType = HandleType.DRAG;
270 | _firstY = motionEvent.getY();
271 | } else {
272 | _handleType = HandleType.NONE;
273 | }
274 | }
275 |
276 | 然后在ACTION_MOVE(即手指按下后并移动)中,判断是往上移动还是往下移动,并实时刷新Y轴的刻度值和电平流的位置。
277 |
278 | private void actionMove(MotionEvent motionEvent) {
279 | if (_handleType == HandleType.DRAG) {
280 | float currentY = motionEvent.getY();
281 |
282 | float spanY = currentY - _firstY;
283 | // 实际在屏幕上滑动的距离,映射到坐标轴Y上的距离
284 | int spanScale = (int) (spanY / ((_height - _marginTop - _marginBottom) / Math.abs(_maxValue - _minValue)));
285 | if (spanScale != 0) {
286 | _offsetY = spanScale;
287 | postInvalidate();
288 | }
289 | }
290 |
291 | 。。。
292 | }
293 |
294 | 此处的_offsetY是关键点,绘制刻度值和电平流就用到的它。
295 |
296 | 
297 |
298 | 多点触摸缩放,在ACTION_POINTER_DOWN(即两个手指按下的事件)中,记录当前的位置的距离_oldDistanceY。
299 |
300 | private void actionPointerDown(MotionEvent motionEvent) {
301 | if (motionEvent.getPointerCount() == 2) {
302 | _handleType = HandleType.ZOOM;
303 | float y1 = motionEvent.getY(0);
304 | float y2 = motionEvent.getY(1);
305 | _oldDistanceY = Math.abs(y1 - y2);
306 | }
307 | }
308 |
309 | 然后在ACTION_MOVE中计算实时的距离currentDistanceY,并根据此计算出_zoomOffsetY,以此实时刷新Y轴的刻度值和电平流的图形。
310 |
311 | private void actionMove(MotionEvent motionEvent) {
312 | if (_handleType == HandleType.DRAG) {
313 | float currentY = motionEvent.getY();
314 |
315 | float spanY = currentY - _firstY;
316 | // 实际在屏幕上滑动的距离,映射到坐标轴Y上的距离
317 | int spanScale = (int) (spanY / ((_height - _marginTop - _marginBottom) / Math.abs(_maxValue - _minValue)));
318 | if (spanScale != 0) {
319 | _offsetY = spanScale;
320 | postInvalidate();
321 | }
322 | } else if (_handleType == HandleType.ZOOM && motionEvent.getPointerCount() == 2) { // 需要加一个判断不然会报错
323 | float y1 = motionEvent.getY(0);
324 | float y2 = motionEvent.getY(1);
325 | float currentDistanceY = Math.abs(y1 - y2);
326 |
327 | float perScaleHeight = (_height - _marginTop - _marginBottom) / (float) Math.abs(_maxValue - _minValue);
328 |
329 | int spanScale = (int) ((currentDistanceY - _oldDistanceY) / perScaleHeight);
330 | if (spanScale != 0 && ((_maxValue - spanScale) - (_minValue + spanScale)) >= _gridCount) { // 防止交叉越界,并且在放大到 总刻度长为 _gridCount 时,不能缩小
331 | _zoomOffsetY = spanScale;
332 | postInvalidate();
333 | }
334 | }
335 | }
336 |
337 | 最后在ACTION_POINTER_UP和ACTION_UP中固化状态。
338 |
339 | private void actionPointerUp(MotionEvent motionEvent) {
340 | if (_handleType == HandleType.ZOOM) {
341 | _maxValue -= _zoomOffsetY;
342 | _minValue += _zoomOffsetY;
343 | _zoomOffsetY = 0;
344 | }
345 |
346 | _handleType = HandleType.NONE;
347 | }
348 |
349 | private void actionCancelUp(MotionEvent motionEvent) {
350 | if (_handleType == HandleType.DRAG) {
351 | _maxValue += _offsetY;
352 | _minValue += _offsetY;
353 | _offsetY = 0;
354 | }
355 |
356 | _handleType = HandleType.NONE;
357 | }
358 |
359 | 还忘了介绍自动调整比列显示,即根据电平的最大值和最小值,计算出Y轴的刻度值,然后再更新电平流图形。
360 |
361 | /**
362 | * 自动调整
363 | */
364 | public void autoView() {
365 | if (_levels.size() == 0)
366 | return;
367 |
368 | // 自动调整视图,就是要找到合理的 _maxValue 和 _minValue
369 |
370 | float sum = 0;
371 | int num = 0;
372 |
373 | for (int i = 0; i < _levels.size(); i++) {
374 | sum += _levels.get(i);
375 | num = i;
376 | }
377 |
378 | float average = sum / num;
379 | float maxValue = Collections.max(_levels);
380 | float minValue = Collections.min(_levels);
381 | num = 0;
382 |
383 | for (int i = 1; i < 1000; i++) {
384 | float max = average + 5 * i;
385 | float min = average - 5 * i;
386 | if (max > maxValue && min < minValue) {
387 | if (Math.abs((max - maxValue) / (float) Math.abs(max - min)) >= 0.25) {
388 | num = i; // 最大值与顶点的距离 >= 1/4
389 | break;
390 | }
391 | }
392 | }
393 |
394 | if (num != 0) {
395 | _maxValue = (int) (average + 5 * num);
396 | _minValue = (int) (average - 5 * num);
397 | postInvalidate();
398 | }
399 | }
400 |
401 | 至此,自定义电平流图形控件的绘制基本完成。当然这仅仅是实现了基本的绘制和展示,在实际的系统中,可能还会添加其他元素,比如电平门限,以及对外提供的一些接口或事件,这就要根据实际的应用场景再进行添加和完善了。
402 |
403 | 好了,本文的介绍就到这里了,我是一个安卓新手,代码中可能有所欠缺或不妥之处,敬请斧正。另外此控件的实现暂未考虑性能方便的问题,不过正常使用也很流畅,后面我要更新的频谱瀑布图,就会加入性能方便的考虑。敬请关注,谢谢。
404 |
405 | 最后奉上此控件的源代码: [https://github.com/AnuoF/android_customview](https://github.com/AnuoF/android_customview)
406 |
407 | AnuoF
408 | Chengdu
409 | Aug 18,2019
--------------------------------------------------------------------------------
/doc/安卓自定义频谱图控件.md:
--------------------------------------------------------------------------------
1 | # 安卓自定义频谱图控件 #
2 |
3 | ## 引言 ##
4 |
5 | 之前我写过一篇文章,讲的是安卓自定义电平流控件的实现,在这片文章中我要讲的是频谱图的实现。相信我们大多数人都接触过或者是知道频谱吧,频谱图就是显示无线电信号在一定带宽范围内,信号强弱的变化,一目了然的可以看到信号有无,或者信号的变化等特征。这里我也就不过多的阐述,接下来主要讲解如何实现安卓客户端上的频谱图控件。
6 |
7 | 首先看下效果图:
8 |
9 | 
10 |
11 | 
12 |
13 | 实现了,最大值、最小值、实时值的绘制,同时Y轴拖动,以及框选显示(X轴缩放)等。
14 |
15 | ## 实现 ##
16 |
17 | ### 布局 ###
18 |
19 | 要画一个图形控件,首先是布局,要画哪些元素,元素的位置布局,元素的颜色、字体等等,这些东西捋清楚之后,就可以动手画了。您在网上随便一搜频谱图,应该就可以看到大致长啥样了,基本都差不多,我的频谱图布局如下:
20 |
21 | 
22 |
23 | 接下来就应该是编码实现了。
24 |
25 | ### 编码 ###
26 |
27 | 首先新建一个类SpectrumView继承View,实现必要的构造函数:
28 |
29 | public class SpectrumView extends View implements View.OnTouchListener {
30 |
31 | public SpectrumView(Context context, AttributeSet attrs, int defStypeAttr) {
32 | super(context, attrs, defStypeAttr);
33 | initView(context, attrs);
34 | }
35 |
36 | public SpectrumView(Context context, AttributeSet attrs) {
37 | super(context, attrs);
38 | initView(context, attrs);
39 | }
40 |
41 | public SpectrumView(Context context) {
42 | super(context);
43 | initView();
44 | }
45 | }
46 |
47 | 在attr.xml中定义控件的属性,便于调用这可以定制某些属性,比如颜色、字体大小等,并在初始化initView()中读取设置的值:
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | ...
62 |
63 | 接下来就是绘制,首先需要重载onMeasure()和onDraw()方法,在onMeasure()中确定View的高和宽:
64 |
65 | _width = getMeasuredWidth();
66 | _height = getMeasuredHeight();
67 |
68 | 在onDraw()中就是具体的绘制了,首先是drawUnit()绘制左中的单位“电平(dBuV)”,drawAxis()绘制坐标轴和网格,这两个个的实现基本和电平流控件的那里完全一样,就不再阐述。至此,基本的背景绘制已完成,效果图如下:
69 |
70 | 
71 |
72 | 当setData()来数据之后,就开始绘制频谱了drawSpectrum():
73 |
74 | /**
75 | * 画频谱
76 | *
77 | * @param canvas
78 | */
79 | private void drawSpectrum(Canvas canvas) {
80 | if (_data.length == 0 || _startIndex >= _endIndex)
81 | return; // 没有数据时不需要绘制
82 |
83 | _paint.setColor(_realTimeLineColor);
84 | _paint.setStyle(Paint.Style.STROKE);
85 |
86 | int maxValue = _maxValue + _offsetY - _zoomOffsetY;
87 | int minValue = _minValue + _offsetY + _zoomOffsetY;
88 |
89 | int scaleHeight = _height - _marginTop - _marginBottom; // 绘制区总高度
90 | int scaleWidth = _width - _marginLeft - _marginRight - _scaleLineLength; // 绘制区总宽度
91 | float perHeight = scaleHeight / (float) Math.abs(maxValue - minValue); // 每一格的高度
92 | float perWidth = scaleWidth / (float) (_endIndex - _startIndex);
93 |
94 | Path realTimePath = new Path();
95 | Path maxValuePath = null;
96 | Path minValuePath = null;
97 |
98 | for (int i = _startIndex; i <= _endIndex; i++) { // 此处需要加上=,确保最后一个点可以绘制
99 | if (i >= _data.length) // 防止越界
100 | continue;
101 |
102 | float level = _data[i];
103 | int x = (int) ((i - _startIndex) * perWidth) + _marginLeft + _scaleLineLength;
104 | int y = (int) ((maxValue - level) * perHeight) + _marginTop;
105 |
106 | if (i == _startIndex) {
107 | realTimePath.moveTo(x, y);
108 | } else {
109 | realTimePath.lineTo(x, y);
110 | }
111 |
112 | if (_drawMaxValue) {
113 | if (maxValuePath == null) {
114 | maxValuePath = new Path();
115 | }
116 |
117 | float maxLevel = _maxData[i];
118 | int max_x = (int) ((i - _startIndex) * perWidth) + _marginLeft + _scaleLineLength;
119 | int max_y = (int) ((maxValue - maxLevel) * perHeight) + _marginTop;
120 |
121 | if (i == _startIndex) {
122 | maxValuePath.moveTo(max_x, max_y);
123 | } else {
124 | maxValuePath.lineTo(max_x, max_y);
125 | }
126 | }
127 |
128 | if (_drawMinValue) {
129 | if (minValuePath == null) {
130 | minValuePath = new Path();
131 | }
132 |
133 | float minLevel = _minData[i];
134 | int min_x = (int) ((i - _startIndex) * perWidth) + _marginLeft + _scaleLineLength;
135 | int min_y = (int) ((maxValue - minLevel) * perHeight) + _marginTop;
136 |
137 | if (i == _startIndex) {
138 | minValuePath.moveTo(min_x, min_y);
139 | } else {
140 | minValuePath.lineTo(min_x, min_y);
141 | }
142 | }
143 | }
144 |
145 | canvas.drawPath(realTimePath, _paint);
146 | if (maxValuePath != null) {
147 | _paint.setColor(_maxValueLineColor);
148 | canvas.drawPath(maxValuePath, _paint); // 画最大值
149 | }
150 | if (minValuePath != null) {
151 | _paint.setColor(_minValueLineColor);
152 | canvas.drawPath(minValuePath, _paint); // 画最小值
153 | }
154 |
155 | // 覆盖上边和下边,使频谱看上去是在指定区域进行绘制的
156 | _paint.setStyle(Paint.Style.FILL);
157 | Drawable background = getBackground();
158 | if (background instanceof ColorDrawable) {
159 | ColorDrawable colorDrawable = (ColorDrawable) background;
160 | int color = colorDrawable.getColor();
161 | _paint.setColor(color);
162 | canvas.drawRect(_marginLeft + _scaleLineLength, 0, _width - _marginRight, _marginTop, _paint);
163 | canvas.drawRect(_marginLeft + _scaleLineLength, _height - _marginBottom + 1, _width - _marginRight, _height + 1, _paint);
164 | }
165 |
166 | // 计算并绘制中心频率和带宽
167 | double perFreq = _spectrumSpan / _data.length / 1000;
168 | double span = perFreq * (_endIndex - _startIndex) / 2 * 1000;
169 |
170 | String centerFreqStr, startFreqStr, endFreqStr;
171 | // 如果是全景,则显示中心频率和带宽,局部缩放则显示起始、终止频率和中心点频率
172 | if (_startIndex == 0 && _endIndex == _data.length) {
173 | centerFreqStr = String.format("%.3f", _frequency) + "MHz";
174 | startFreqStr = "-" + String.format("%.3f", span) + "kHz";
175 | endFreqStr = "+" + String.format("%.3f", span) + "kHz";
176 | } else {
177 | int centerIndex = (_startIndex + (_endIndex - _startIndex) / 2);
178 | centerFreqStr = String.format("%.3f", centerIndex * perFreq + (_frequency - _spectrumSpan / 2 / 1000)) + " MHz";
179 | startFreqStr = String.format("%.3f", _startIndex * perFreq + (_frequency - _spectrumSpan / 2 / 1000)) + " MHz";
180 | endFreqStr = String.format("%.3f", _endIndex * perFreq + (_frequency - _spectrumSpan / 2 / 1000)) + " MHz";
181 | }
182 |
183 | Rect freqRect = new Rect();
184 | _paint.setColor(_gridColor);
185 | _paint.getTextBounds(centerFreqStr, 0, centerFreqStr.length(), freqRect);
186 | canvas.drawText(centerFreqStr, _width - _marginRight - scaleWidth / 2 - freqRect.width() / 2, _height - _marginBottom + freqRect.height() + 5, _paint);
187 | canvas.drawText(startFreqStr, _marginLeft + _scaleLineLength, _height - _marginBottom + freqRect.height() + 5, _paint);
188 | canvas.drawText(endFreqStr, _width - _marginRight - (float) _paint.measureText(endFreqStr), _height - _marginBottom + freqRect.height() + 5, _paint);
189 | }
190 |
191 | 这里的关键点在于计算出幅度与屏幕上所在的位置,并不复杂,具体可以参见代码,画了Path之后,在上边和下边各画一个矩形,覆盖在上面,这样当频谱的图形移动到上面去的时候并不会越过网格界限,看起来就是绘制在网格以内,实际上您要是不绘制矩形的话,可以试试,看下又会是什么效果。
192 |
193 | 画到这里,频谱图的静态展示效果就完成了,这还没完,还需要加上一些交互事件:图形上下拖动,局部缩放等。下面是具体实现步骤,先重载onTouch()方法:
194 |
195 | @Override
196 | public boolean onTouch(View view, MotionEvent motionEvent) {
197 | switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
198 | case MotionEvent.ACTION_DOWN:
199 | actionDown(motionEvent);
200 | break;
201 | case MotionEvent.ACTION_POINTER_DOWN:
202 | actionPointerDown(motionEvent);
203 | break;
204 | case MotionEvent.ACTION_MOVE:
205 | actionMove(motionEvent);
206 | break;
207 | case MotionEvent.ACTION_POINTER_UP:
208 | actionPointerUp(motionEvent);
209 | break;
210 | case MotionEvent.ACTION_CANCEL:
211 | case MotionEvent.ACTION_UP:
212 | actionCancelUp(motionEvent);
213 | break;
214 | }
215 |
216 | return true;
217 | }
218 |
219 | 在ACTION_DOWN中,主要根据手指按下的位置,来确定当前操作是Y轴拖动还是X轴缩放:
220 |
221 | private void actionDown(MotionEvent event) {
222 | if (Utils.IsPointInRect(0, 0, _marginLeft + _scaleLineLength, _height, (int) event.getX(), (int) event.getY())) {
223 | _handleType = HandleType.DRAG; // 纵轴拖动
224 | _startY = event.getY();
225 | } else if (Utils.IsPointInRect(_marginLeft + _scaleLineLength, _marginTop, _width - _marginRight, _height - _marginBottom, (int) event.getX(), (int) event.getY())) {
226 | _handleType = HandleType.ZONE; // 缩放频谱
227 | _startX = _endX = event.getX();
228 | }
229 | }
230 |
231 | 在ACTION_POINTER_DOWN中主要是记录两个手指按下时的初始距离:
232 |
233 | private void actionPointerDown(MotionEvent event) {
234 | if (event.getPointerCount() == 2) {
235 | _handleType = HandleType.ZOOM;
236 | _oldDistanceY = Math.abs(event.getY(0) - event.getY(1));
237 | }
238 | }
239 |
240 | 在ACTION_MOVE中主要是根据当前的操作类型,来确定_offsetY/_zoomOffsetY/_endX等字段的值,并实时刷新绘制图形:
241 |
242 | private void actionMove(MotionEvent event) {
243 | if (_handleType == HandleType.DRAG) {
244 | float currrentY = event.getY();
245 | int spanScale = (int) ((currrentY - _startY) / ((_height - _marginTop - _marginBottom) / Math.abs((_maxValue - _minValue))));
246 | if (spanScale != 0) {
247 | _offsetY = spanScale;
248 | postInvalidate();
249 | }
250 | } else if (_handleType == HandleType.ZOOM && event.getPointerCount() == 2) {
251 | float currentDistanceY = Math.abs(event.getY(0) - event.getY(1));
252 | float perScaleHeight = (_height - _marginTop - _marginBottom) / (float) Math.abs(_maxValue - _minValue);
253 | int spanScale = (int) ((currentDistanceY - _oldDistanceY) / perScaleHeight);
254 | if (spanScale != 0 && ((_maxValue - spanScale) - (_minValue + spanScale) >= _gridCount)) { // 防止交叉越界,并且在放大到 总刻度长为 _gridCount 时,不能再放大
255 | _zoomOffsetY = spanScale;
256 | postInvalidate();
257 | }
258 | } else if (_handleType == HandleType.ZONE) {
259 | _endX = event.getX();
260 | if (_endX < _marginLeft + _scaleLineLength) {
261 | _endX = _marginLeft + _scaleLineLength;
262 | } else if (_endX > _width - _marginRight) {
263 | _endX = _width - _marginRight;
264 | } // 此处的判断是为了防止Rect越界
265 |
266 | postInvalidate();
267 | }
268 | }
269 |
270 | 在ACTION_POINTER_UP和ACTION_CANCEL或ACTION_UP中,主要是固化字段的状态。
271 |
272 | private void actionPointerUp(MotionEvent event) {
273 | if (_handleType == HandleType.ZOOM) {
274 | _maxValue -= _zoomOffsetY;
275 | _minValue += _zoomOffsetY;
276 | _zoomOffsetY = 0;
277 | }
278 |
279 | _handleType = HandleType.NONE;
280 | }
281 |
282 | private void actionCancelUp(MotionEvent event) {
283 | if (_handleType == HandleType.DRAG) {
284 | _maxValue += _offsetY;
285 | _minValue += _offsetY;
286 | _offsetY = 0;
287 | } else if (_handleType == HandleType.ZONE) {
288 | // 这里需要读取索引
289 | if (_startX > _endX) {
290 | // 缩小
291 | _startIndex = 0;
292 | _endIndex = _data.length;
293 | postInvalidate();
294 | } else if (_startX < _endX) {
295 | // 放大。 根据 _startX 和 _endX 来确定 _startIndex 和 _endIndex,以及中心频率和带宽
296 | if (_data.length == 0 || _endIndex - _startIndex <= 2) { // 没有数据,或者只要小于2个点时,不再放大
297 | _handleType = HandleType.NONE;
298 | return;
299 | }
300 |
301 | float perScaleLength = (_width - _marginLeft - _scaleLineLength - _marginRight) / (float) (_endIndex - _startIndex); // 一格的距离
302 | // 在放大的基础上再次放大,巧妙啊,佩服我自己了,哈哈哈
303 | int tempEndIndex = _startIndex + (int) ((_endX - _marginLeft - _scaleLineLength) / perScaleLength);
304 | int tempStartIndex = _startIndex + (int) ((_startX - _marginLeft - _scaleLineLength) / perScaleLength);
305 | if (tempEndIndex > tempStartIndex) { // 保证至少有2个点(一条直线)
306 | _endIndex = tempEndIndex;
307 | _startIndex = tempStartIndex;
308 | postInvalidate();
309 | }
310 | }
311 | }
312 |
313 | _handleType = HandleType.NONE;
314 | }
315 |
316 | 至此,频谱控件的交互事件也基本完成。最后,我们再对外提供一些方法,便于调用:
317 |
318 | public void setData(double frequency, double spectrunSpan, float[] data);
319 | public void offsetY(int offset);
320 | public void zoomY(int zoom);
321 | public void clear();
322 | public void autoView();
323 | public void setMaxValueLineVisible(boolean visible);
324 | public void setMinValueLineVisible(boolean visible);
325 |
326 | 好了,控件的大致实现过程就是如上所述,程序也并不复杂,只要细心点,把各种情况的考虑下,就没啥问题。
327 |
328 | 最后,如果要集成使用控件,可能还需添加其他功能或方法,才能完善系统,可以定制开发,如果有需要可以联系本人。
329 |
330 | 最后1,我的联系方式,呃,,如果您真的需要的话,随便那种方式应该都可以找到我的,哈哈,提醒一下,在代码里面也有,例如:
331 |
332 | /**
333 | * @Title: SpectrumView.java
334 | * @Package: com.an.view
335 | * @Description: 自定义频谱图控件
336 | * @Author: AnuoF
337 | * @QQ/WeChat: 188512936
338 | * @Date 2019.08.09 20:27
339 | * @Version V1.0
340 | */
341 | 最后2:奉上源码,自由、开源:[https://github.com/AnuoF/android_customview](https://github.com/AnuoF/android_customview)
342 |
343 | 最后,定稿!!!!
344 |
345 | AnuoF
346 | Chengdu
347 | Aug 20,2019
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnuoF/android_customview/53de55289fabf563c94be2169b63154882e7baf0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Aug 22 08:32:48 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------