├── .idea
├── .name
├── copyright
│ └── profiles_settings.xml
├── dictionaries
│ └── enniu.xml
├── encodings.xml
├── inspectionProfiles
│ ├── profiles_settings.xml
│ └── Project_Default.xml
├── runConfigurations.xml
├── modules.xml
├── compiler.xml
├── gradle.xml
└── misc.xml
├── demo
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ └── layout
│ │ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── bupt
│ │ │ └── edison
│ │ │ └── magicreddotdemo
│ │ │ └── MainActivity.java
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── bupt
│ │ │ └── edison
│ │ │ └── magicreddotdemo
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── bupt
│ │ └── edison
│ │ └── magicreddotdemo
│ │ └── ApplicationTest.java
├── proguard-rules.pro
└── build.gradle
├── magicreddot
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── attrs.xml
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── pop1.png
│ │ │ │ ├── pop2.png
│ │ │ │ ├── pop3.png
│ │ │ │ ├── pop4.png
│ │ │ │ └── pop5.png
│ │ │ └── drawable
│ │ │ │ └── dismiss_anim.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── bupt
│ │ │ └── edison
│ │ │ └── magicreddot
│ │ │ ├── Utils.java
│ │ │ ├── MathUtils.java
│ │ │ └── MagicRedDotView.java
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── bupt
│ │ │ └── edison
│ │ │ └── magicreddot
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── bupt
│ │ └── edison
│ │ └── magicreddot
│ │ └── ApplicationTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── screenshot
├── QQRedDotView.gif
├── QQRedDotView_common.png
├── QQRedDotView_qqdot.gif
├── QQRedDotView_solid.png
└── QQRedDotView_updateMsgCount.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | QQRedDotDemo
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/magicreddot/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':demo', ':magicreddot'
2 |
--------------------------------------------------------------------------------
/screenshot/QQRedDotView.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/screenshot/QQRedDotView.gif
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/dictionaries/enniu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | MagicRedDotDemo
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/screenshot/QQRedDotView_common.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/screenshot/QQRedDotView_common.png
--------------------------------------------------------------------------------
/screenshot/QQRedDotView_qqdot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/screenshot/QQRedDotView_qqdot.gif
--------------------------------------------------------------------------------
/screenshot/QQRedDotView_solid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/screenshot/QQRedDotView_solid.png
--------------------------------------------------------------------------------
/magicreddot/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | MagicRedDot
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/screenshot/QQRedDotView_updateMsgCount.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/screenshot/QQRedDotView_updateMsgCount.gif
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/magicreddot/src/main/res/mipmap-xhdpi/pop1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/magicreddot/src/main/res/mipmap-xhdpi/pop1.png
--------------------------------------------------------------------------------
/magicreddot/src/main/res/mipmap-xhdpi/pop2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/magicreddot/src/main/res/mipmap-xhdpi/pop2.png
--------------------------------------------------------------------------------
/magicreddot/src/main/res/mipmap-xhdpi/pop3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/magicreddot/src/main/res/mipmap-xhdpi/pop3.png
--------------------------------------------------------------------------------
/magicreddot/src/main/res/mipmap-xhdpi/pop4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/magicreddot/src/main/res/mipmap-xhdpi/pop4.png
--------------------------------------------------------------------------------
/magicreddot/src/main/res/mipmap-xhdpi/pop5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/magicreddot/src/main/res/mipmap-xhdpi/pop5.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanglongba/MagicRedDot/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jul 26 22:08:17 CST 2016
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-2.14.1-bin.zip
7 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/magicreddot/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/demo/src/test/java/com/bupt/edison/magicreddotdemo/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.bupt.edison.magicreddotdemo;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect() throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/magicreddot/src/test/java/com/bupt/edison/magicreddot/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.bupt.edison.magicreddot;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect() throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/bupt/edison/magicreddotdemo/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.bupt.edison.magicreddotdemo;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/magicreddot/src/androidTest/java/com/bupt/edison/magicreddot/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.bupt.edison.magicreddot;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/magicreddot/src/main/res/drawable/dismiss_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
11 |
14 |
17 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/demo/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/enniu/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/magicreddot/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/enniu/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/demo/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 23
5 | buildToolsVersion "24.0.1"
6 |
7 | defaultConfig {
8 | applicationId "com.bupt.edison.magicreddotdemo"
9 | minSdkVersion 14
10 | targetSdkVersion 23
11 | versionCode 1
12 | versionName "1.0"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | compile fileTree(include: ['*.jar'], dir: 'libs')
24 | testCompile 'junit:junit:4.12'
25 | compile 'com.android.support:appcompat-v7:23.4.0'
26 | compile project(':magicreddot')
27 |
28 | compile 'com.jakewharton:butterknife:7.0.1'
29 | }
30 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## Project-wide Gradle settings.
2 | #
3 | # For more details on how to configure your build environment visit
4 | # http://www.gradle.org/docs/current/userguide/build_environment.html
5 | #
6 | # Specifies the JVM arguments used for the daemon process.
7 | # The setting is particularly useful for tweaking memory settings.
8 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
9 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
10 | #
11 | # When configured, Gradle will run in incubating parallel mode.
12 | # This option should only be used with decoupled projects. More details, visit
13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
14 | # org.gradle.parallel=true
15 | #Tue Jul 26 16:41:35 CST 2016
16 | systemProp.http.proxyHost=127.0.0.1
17 | systemProp.http.proxyPort=8787
18 |
--------------------------------------------------------------------------------
/demo/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/gradle.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 |
--------------------------------------------------------------------------------
/magicreddot/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 |
--------------------------------------------------------------------------------
/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 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Android
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 |
--------------------------------------------------------------------------------
/magicreddot/src/main/java/com/bupt/edison/magicreddot/Utils.java:
--------------------------------------------------------------------------------
1 | package com.bupt.edison.magicreddot;
2 |
3 | import android.content.Context;
4 | import android.graphics.Paint;
5 | import android.graphics.Rect;
6 | import android.text.TextUtils;
7 | import android.util.TypedValue;
8 | import android.view.View;
9 | import android.view.Window;
10 |
11 | /**
12 | * Created by edison on 16/7/3.
13 | */
14 | public class Utils {
15 | /**
16 | * dip转化为px
17 | * @param context
18 | * @param dipValue
19 | * @return
20 | */
21 | public static int dp2px(Context context, int dipValue){
22 | if (context == null) {
23 | return dipValue;
24 | }
25 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, context.getResources().getDisplayMetrics());
26 |
27 | }
28 |
29 | /**
30 | * 将sp值转换为px值,保证文字大小不变
31 | *
32 | * @param context
33 | * @param spValue
34 | * @return
35 | */
36 | public static int sp2px(Context context, float spValue) {
37 | if (context == null) {
38 | return (int)spValue;
39 | }
40 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, context.getResources().getDisplayMetrics());
41 | }
42 |
43 | /**
44 | * 获取标题栏高度
45 | * @param window
46 | * @return
47 | */
48 | public static int getTitleBarHeight(Window window){
49 | int contentTop = window.findViewById(Window.ID_ANDROID_CONTENT).getTop();
50 | return contentTop - getStatusBarHeight(window);
51 | }
52 |
53 | /**
54 | * 获取状态栏的高度
55 | * @param view
56 | * @return
57 | */
58 | public static int getStatusBarHeight(View view) {
59 | Rect rect = new Rect();
60 | view.getWindowVisibleDisplayFrame(rect);
61 | return rect.top;
62 | }
63 |
64 | /**
65 | * 获取状态栏的高度
66 | * @param window
67 | * @return
68 | */
69 | public static int getStatusBarHeight(Window window){
70 | Rect frame = new Rect();
71 | window.getDecorView().getWindowVisibleDisplayFrame(frame);
72 | return frame.top;
73 | }
74 |
75 | /**
76 | * 计算字符串的绘制宽度
77 | *
78 | * @param paint
79 | * @param str
80 | * @return
81 | */
82 | public static int computeStringWidth(Paint paint, String str) {
83 | int iRet = 0;
84 | if (!TextUtils.isEmpty(str)) {
85 | int len = str.length();
86 | float[] widths = new float[len];
87 | paint.getTextWidths(str, widths);
88 | for (int j = 0; j < len; j++) {
89 | iRet += (int) Math.ceil(widths[j]);
90 | }
91 | }
92 | // Log.d("edison","text width: "+iRet);
93 | return iRet;
94 | }
95 |
96 | /**
97 | * 计算字符串的绘制高度
98 | *
99 | * @param paint
100 | * @param string
101 | * @return 只有这种方法可以计算字符串的高度
102 | */
103 | public static int computeStringHeight(Paint paint, String string) {
104 | Rect rect = new Rect();
105 |
106 | //返回包围整个字符串的最小的一个Rect区域
107 | paint.getTextBounds(string, 0, 1, rect);
108 | // Log.d("edison","text height: "+rect.height());
109 | return rect.height();
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/magicreddot/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | //应用插件
3 | apply plugin: 'com.github.dcendents.android-maven'
4 | apply plugin: 'com.jfrog.bintray'
5 |
6 | android {
7 | compileSdkVersion 23
8 | buildToolsVersion "24.0.1"
9 |
10 | defaultConfig {
11 | minSdkVersion 14
12 | targetSdkVersion 23
13 | versionCode 1
14 | versionName "1.0"
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 | }
23 |
24 | dependencies {
25 | compile fileTree(dir: 'libs', include: ['*.jar'])
26 | testCompile 'junit:junit:4.12'
27 | compile 'com.android.support:appcompat-v7:23.4.0'
28 | }
29 |
30 | //定义公共参数
31 | def siteUrl = 'https://github.com/kanglongba/MagicRedDot' // 项目的主页
32 | def gitUrl = 'https://github.com/kanglongba/MagicRedDot.git' // Git仓库的url
33 |
34 | //配置android-maven-gradle-plugin插件
35 | group = "com.bupt.edison.magicreddot" // groupId,一般由host+artifactId组成
36 | version = "1.0.0" //版本号,以后更新library时,它也要跟着增加
37 |
38 | //配置android-maven-gradle-plugin插件
39 | install {
40 | repositories.mavenInstaller {
41 | // This generates POM.xml with proper parameters
42 | pom {
43 | project {
44 | packaging 'aar'
45 |
46 | // Add your description here
47 | name 'Android Magic Dot' //项目的名称
48 | description 'a powerful red dot widget for android' //项目的描述
49 | url siteUrl
50 |
51 | // Set your license
52 | licenses {
53 | license {
54 | name 'The Apache Software License, Version 2.0'
55 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
56 | }
57 | }
58 |
59 | developers {
60 | developer {
61 | id 'kanglongba' //bintray的username
62 | name 'edison' //姓名
63 | email 'kanglongba@gmail.com' //邮箱
64 | }
65 | }
66 |
67 | scm {
68 | connection gitUrl
69 | developerConnection gitUrl
70 | url siteUrl
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
77 | task sourcesJar(type: Jar) {
78 | from android.sourceSets.main.java.srcDirs
79 | classifier = 'sources'
80 | }
81 |
82 | task javadoc(type: Javadoc) {
83 | source = android.sourceSets.main.java.srcDirs
84 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
85 | }
86 |
87 | task javadocJar(type: Jar, dependsOn: javadoc) {
88 | classifier = 'javadoc'
89 | from javadoc.destinationDir
90 | }
91 |
92 | artifacts {
93 | archives javadocJar
94 | archives sourcesJar
95 | }
96 |
97 |
98 | //配置gradle-bintray-plugin插件
99 | Properties properties = new Properties()
100 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
101 |
102 | //对gradle-bintray-plugin插件的配置,全部写在 bintray{ ... } 中
103 | bintray {
104 | user = properties.getProperty("bintray.user")
105 | key = properties.getProperty("bintray.apikey")
106 |
107 | configurations = ['archives']
108 |
109 | pkg {
110 | repo = "maven"
111 | name = "magicreddot" //artifactId,会作为library的名称显示在JCenter中
112 | websiteUrl = siteUrl
113 | vcsUrl = gitUrl
114 | licenses = ["Apache-2.0"]
115 | publish = true
116 |
117 | version {
118 | desc = 'a powerful red dot widget for android'
119 | gpg {
120 | sign = true //Determines whether to GPG sign the files. The default is false
121 | passphrase = properties.getProperty("bintray.gpg.password") //Optional. The passphrase for GPG signing'
122 | }
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/bupt/edison/magicreddotdemo/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.bupt.edison.magicreddotdemo;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.util.Log;
6 | import android.view.View;
7 | import android.widget.Button;
8 | import android.widget.TextView;
9 | import android.widget.Toast;
10 |
11 | import com.bupt.edison.magicreddot.MagicRedDotView;
12 | import com.bupt.edison.magicreddot.Utils;
13 |
14 | import butterknife.Bind;
15 | import butterknife.ButterKnife;
16 |
17 | public class MainActivity extends AppCompatActivity {
18 |
19 | @Bind(R.id.qqdot_0)
20 | MagicRedDotView qqdot0;
21 | @Bind(R.id.qqdot_1)
22 | MagicRedDotView qqdot1;
23 | @Bind(R.id.qqdot_2)
24 | MagicRedDotView qqdot2;
25 | @Bind(R.id.qqdot_3)
26 | MagicRedDotView qqdot3;
27 | @Bind(R.id.qqdot_4)
28 | MagicRedDotView qqdot4;
29 | @Bind(R.id.btn_updateMsg)
30 | Button btnUpdateMsg;
31 | @Bind(R.id.qqdot_5)
32 | MagicRedDotView qqdot5;
33 | @Bind(R.id.qqdot_6)
34 | MagicRedDotView qqdot6;
35 | @Bind(R.id.textMsgCount)
36 | TextView textMsgCount;
37 |
38 | @Override
39 | protected void onCreate(Bundle savedInstanceState) {
40 | super.onCreate(savedInstanceState);
41 | setContentView(R.layout.activity_main);
42 | ButterKnife.bind(this);
43 |
44 | setQQRedDotViewWithText();
45 | setQQRedDotView();
46 | }
47 |
48 | private void setQQRedDotViewWithText() {
49 | qqdot0.setUnreadCount(9);
50 | qqdot1.setUnreadCount(279);
51 | qqdot2.setUnreadCount(100);
52 | qqdot3.setUnreadCount(89);
53 | qqdot4.setUnreadCount(301);
54 |
55 | btnUpdateMsg.setOnClickListener(new View.OnClickListener() {
56 | @Override
57 | public void onClick(View v) {
58 | int f = (int) (Math.random() * 10);
59 | int msgCount = 1;
60 | switch (f % 4) {
61 | case 0:
62 | msgCount = (int) (Math.random() * 10); //个位数
63 | break;
64 | case 1:
65 | msgCount = (int) (Math.random() * 100);
66 | break;
67 | case 2:
68 | msgCount = (int) (Math.random() * 1000);
69 | break;
70 | case 3:
71 | msgCount = (int) (Math.random() * 1000);
72 | break;
73 | }
74 | Log.d("edison", "count " + msgCount);
75 | qqdot5.setUnreadCount(msgCount);
76 | textMsgCount.setText(String.valueOf(msgCount));
77 | }
78 | });
79 | }
80 |
81 | private void setQQRedDotView() {
82 | qqdot6.setUnreadCount(666);
83 | qqdot6.setOnDragStartListener(new MagicRedDotView.OnDragStartListener() {
84 | @Override
85 | public void OnDragStart() {
86 | Toast.makeText(MainActivity.this, "开始拖拽", Toast.LENGTH_SHORT).show();
87 | }
88 | });
89 | qqdot6.setOnDotResetListener(new MagicRedDotView.OnDotResetListener() {
90 | @Override
91 | public void OnDotReset() {
92 | Toast.makeText(MainActivity.this, "红点复位", Toast.LENGTH_SHORT).show();
93 | }
94 | });
95 | qqdot6.setOnDotDismissListener(new MagicRedDotView.OnDotDismissListener() {
96 | @Override
97 | public void OnDotDismiss() {
98 | Toast.makeText(MainActivity.this, "红点消失", Toast.LENGTH_SHORT).show();
99 | }
100 | });
101 | }
102 |
103 | @Override
104 | protected void onResume() {
105 | super.onResume();
106 | //这里还有个坑,状态栏和标题栏的高度计算结果都是0
107 | Log.d("edison titlebar", Utils.getTitleBarHeight(getWindow()) + "");
108 | Log.d("edison statusbar", Utils.getStatusBarHeight(getWindow()) + "");
109 | Log.d("edison statusbar", Utils.getStatusBarHeight(qqdot6) + "");
110 | }
111 |
112 | @Override
113 | protected void onDestroy() {
114 | super.onDestroy();
115 | ButterKnife.unbind(this);
116 | }
117 |
118 | @Override
119 | protected void onStart() {
120 | super.onStart();
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android-MagicRedDot
2 | Android平台,一站式小红点解决方案,先看效果图:
3 |
4 | 
5 |
6 | PS,原本只是想写一个仿新版QQ可拖拽小红点的控件,然后一路写下来,发现可以扩展成一站式小红点解决方案,于是就有了这个控件。
7 |
8 |
9 | # Introduction
10 |
11 | 控件可配置的属性有:
12 |
13 | * 红点的模式
14 | * 实心红点:不可拖动,不显示数字,最基本的红点形式,类似于一张小红点图片
15 | * 普通红点:显示数字,且大小可以随数字的大小而变化,但是不可拖动
16 | * QQ红点:显示数字,大小可随数字的大小而变化,可拖动,拖动过程中,会有橡皮筋效果,且可对拖拽过程设置监听
17 | * 红点的半径
18 | * 锚点(拖拽时固定在屏幕上的点)的半径
19 | * 锚点的半径不能大于红点的半径
20 | * 红点的颜色
21 | * 红点只是一个名字,它可以是任意颜色
22 | * 数字的颜色
23 | * 数字的大小
24 | * 数字的显示方式
25 | * 准确显示:如实显示数字
26 | * 模糊显示:当超过设置的阈值时,只显示一个范围,例如:99+,199+
27 | * 阈值
28 | * 阈值不能小于99,否则会被强制设为99.而且,只有在模糊模式下,才有效果
29 | * 红点可拖动的距离
30 | * 在这个范围内,会有一条皮筋连接红点与锚点
31 | * 超过这个范围后,皮筋会断裂
32 |
33 |
34 | # Usage
35 |
36 | ### 添加依赖
37 |
38 | > compile 'com.bupt.edison.magicreddot:magicreddot:1.0.0'
39 |
40 | ### 在layout xml中使用
41 |
42 | * QQ红点
43 | 1. 在XML文件中设置属性
44 |
45 | ```
46 |
58 | ```
59 | 2. 在代码中更新数字和设置监听器
60 |
61 | ```
62 | //更新数字
63 | qqdot6.setUnreadCount(666);
64 | //开始拖动的监听
65 | qqdot6.setOnDragStartListener(new MagicRedDotView.OnDragStartListener() {
66 | @Override
67 | public void OnDragStart() {
68 | Toast.makeText(MainActivity.this, "开始拖拽", Toast.LENGTH_SHORT).show();
69 | }
70 | });
71 | //复位的监听
72 | qqdot6.setOnDotResetListener(new MagicRedDotView.OnDotResetListener() {
73 | @Override
74 | public void OnDotReset() {
75 | Toast.makeText(MainActivity.this, "红点复位", Toast.LENGTH_SHORT).show();
76 | }
77 | });
78 | //消失的监听
79 | qqdot6.setOnDotDismissListener(new MagicRedDotView.OnDotDismissListener() {
80 | @Override
81 | public void OnDotDismiss() {
82 | Toast.makeText(MainActivity.this, "红点消失", Toast.LENGTH_SHORT).show();
83 | }
84 | });
85 | ```
86 | 3. 效果图
87 | 
88 |
89 | * 普通红点
90 | 1. 在xml中设置属性
91 |
92 | ```
93 |
105 | ```
106 | 2. 在代码中更新数字
107 |
108 | ```
109 | qqdot5.setUnreadCount(msgCount);
110 | ```
111 | 3. 效果图
112 | 
113 |
114 | * 实心红点
115 | 1. 在xml中设置属性
116 |
117 | ```
118 |
125 | ```
126 | 2. 不需要在代码中特别设置什么
127 | 3. 效果图
128 | 
129 |
130 |
131 | # Reference
132 |
133 | * [QQ手机版 5.0“一键下班”设计小结](https://isux.tencent.com/qq-mobile-off-duty.html)
134 | * [贝塞尔曲线扫盲](http://www.html-js.com/article/1628)
135 | * [Path之贝塞尔曲线](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B6%5DPath_Bezier.md)
136 | * [类似QQ的小红点](https://github.com/mabeijianxi/stickyDots)
137 |
138 | # License
139 |
140 | Copyright 2016 qianhailong
141 |
142 | Licensed under the Apache License, Version 2.0 (the "License");
143 | you may not use this file except in compliance with the License.
144 | You may obtain a copy of the License at
145 |
146 | http://www.apache.org/licenses/LICENSE-2.0
147 |
148 | Unless required by applicable law or agreed to in writing, software
149 | distributed under the License is distributed on an "AS IS" BASIS,
150 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
151 | See the License for the specific language governing permissions and
152 | limitations under the License.
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
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 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/magicreddot/src/main/java/com/bupt/edison/magicreddot/MathUtils.java:
--------------------------------------------------------------------------------
1 | package com.bupt.edison.magicreddot;
2 |
3 | import android.graphics.PointF;
4 |
5 | /**
6 | * Created by edison on 16/7/3.
7 | */
8 | public class MathUtils {
9 |
10 | /**
11 | * 返回两点之间的距离
12 | * @param x1
13 | * @param y1
14 | * @param x2
15 | * @param y2
16 | * @return
17 | */
18 | public static float getDistanceBetweenPoints(float x1,float y1,float x2,float y2){
19 | return (float)Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2));
20 | }
21 |
22 | /**
23 | * 返回两点的中点
24 | * @param x1
25 | * @param y1
26 | * @param x2
27 | * @param y2
28 | * @return
29 | * 把中点当成贝塞尔曲线的控制点
30 | */
31 | public static PointF getMiddlePoint(float x1,float y1,float x2,float y2){
32 | PointF pointF = new PointF((x1+x2)/2.0f,(y1+y2)/2.0f);
33 | return pointF;
34 | }
35 |
36 | /**
37 | * 获取两点之间的斜率
38 | * @param x1
39 | * @param y1
40 | * @param x2
41 | * @param y2
42 | * @return
43 | */
44 | public static double getGradient(float x1,float y1,float x2,float y2){
45 | float dy = Math.abs(y2-y1);
46 | float dx = Math.abs(x2-x1);
47 | double arcT = Math.atan(dy/dx);
48 | return arcT;
49 | }
50 |
51 | /**
52 | * 计算拖拽时,贝塞尔曲线的四个数据点
53 | * http://isux.tencent.com/wp-content/uploads/2014/10/201410171340142.png
54 | * center2是拖拽点,center1是固定点.一般radius2要大于radius1.
55 | *
56 | * @param centerX1
57 | * @param centerY1
58 | * @param radius1
59 | * @param centerX2
60 | * @param centerY2
61 | * @param radius2
62 | * @return
63 | */
64 | public static PointF[] getTangentPoint(float centerX1,float centerY1,float radius1,float centerX2,float centerY2,float radius2){
65 | PointF[] pointFs = new PointF[4];
66 | PointF p1 = new PointF();
67 | PointF p2 = new PointF();
68 | PointF p3 = new PointF();
69 | PointF p4 = new PointF();
70 |
71 | double arcGradient = getGradient(centerX1,centerY1,centerX2,centerY2); //因为象限的不同,斜率是会变的.所以要分象限计算
72 | double arcP2C2C1 = Math.acos((radius2-radius1)/getDistanceBetweenPoints(centerX1,centerY1,centerX2,centerY2));
73 |
74 | double arcP2C2X;
75 | if(centerX1>=centerX2&¢erY1>=centerY2){//三象限
76 | arcP2C2X = Math.PI - arcP2C2C1 - arcGradient;
77 | p2.set((float)(centerX2-radius2*Math.cos(arcP2C2X)),(float)(centerY2+radius2*Math.sin(arcP2C2X)));
78 | }else if(centerX1<=centerX2&¢erY1<=centerY2){//一象限
79 | arcP2C2X = Math.PI - arcP2C2C1 - arcGradient;
80 | p2.set((float)(centerX2+radius2*Math.cos(arcP2C2X)),(float)(centerY2-radius2*Math.sin(arcP2C2X)));
81 | }else if(centerX1centerY2){ //四象限
82 | arcP2C2X = arcP2C2C1-arcGradient;
83 | p2.set((float)(centerX2-radius2*Math.cos(arcP2C2X)),(float)(centerY2-radius2*Math.sin(arcP2C2X)));
84 | }else{ //二象限
85 | arcP2C2X = arcP2C2C1-arcGradient;
86 | p2.set((float)(centerX2+radius2*Math.cos(arcP2C2X)),(float)(centerY2+radius2*Math.sin(arcP2C2X)));
87 | }
88 |
89 | double arcP1C1C2 = Math.PI - arcP2C2C1;
90 | double arcP1C1X;
91 | if(centerX1>=centerX2&¢erY1>=centerY2){//三象限
92 | arcP1C1X = arcP1C1C2 - arcGradient;
93 | p1.set((float)(centerX1-radius1*Math.cos(arcP1C1X)),(float)(centerY1+radius1*Math.sin(arcP1C1X)));
94 | }else if(centerX1<=centerX2&¢erY1<=centerY2){ //一象限
95 | arcP1C1X = arcP1C1C2 - arcGradient;
96 | p1.set((float)(centerX1+radius1*Math.cos(arcP1C1X)),(float)(centerY1-radius1*Math.sin(arcP1C1X)));
97 | }else if(centerX1centerY2){ //四象限
98 | arcP1C1X = arcP2C2C1-arcGradient;
99 | p1.set((float)(centerX1-radius1*Math.cos(arcP1C1X)),(float)(centerY1-radius1*Math.sin(arcP1C1X)));
100 | }else{ //二象限
101 | arcP1C1X = arcP2C2C1-arcGradient;
102 | p1.set((float)(centerX1+radius1*Math.cos(arcP1C1X)),(float)(centerY1+radius1*Math.sin(arcP1C1X)));
103 | }
104 |
105 | //p3,p4与p1,p2是对称的
106 | double arcC1C2Y = Math.PI/2-arcGradient;
107 | double arcP4C2Y;
108 | if(centerX1>=centerX2&¢erY1>=centerY2){ //三象限
109 | arcP4C2Y = Math.PI/2 - arcP2C2C1 + arcGradient;
110 | p4.set((float)(centerX2+radius2*Math.sin(arcP4C2Y)),(float)(centerY2-radius2*Math.cos(arcP4C2Y)));
111 | }else if(centerX1<=centerX2&¢erY1<=centerY2){ //一象限
112 | arcP4C2Y = Math.PI/2 - arcP2C2C1 + arcGradient;
113 | p4.set((float)(centerX2-radius2*Math.sin(arcP4C2Y)),(float)(centerY2+radius2*Math.cos(arcP4C2Y)));
114 | }else if(centerX1centerY2){ //四象限
115 | arcP4C2Y = arcP2C2C1 - arcC1C2Y;
116 | p4.set((float)(centerX2+radius2*Math.sin(arcP4C2Y)),(float)(centerY2+radius2*Math.cos(arcP4C2Y)));
117 | }else{//二象限
118 | arcP4C2Y = arcP2C2C1 - arcC1C2Y;
119 | p4.set((float)(centerX2-radius2*Math.sin(arcP4C2Y)),(float)(centerY2-radius2*Math.cos(arcP4C2Y)));
120 | }
121 |
122 | double arcP3C1X;
123 | if(centerX1>=centerX2&¢erY1>=centerY2){//三象限
124 | arcP3C1X = Math.PI - arcP1C1C2 - arcGradient;
125 | p3.set((float)(centerX1+radius1*Math.cos(arcP3C1X)),(float)(centerY1-radius1*Math.sin(arcP3C1X)));
126 | }else if(centerX1<=centerX2&¢erY1<=centerY2){//一象限
127 | arcP3C1X = Math.PI - arcP1C1C2 - arcGradient;
128 | p3.set((float)(centerX1-radius1*Math.cos(arcP3C1X)),(float)(centerY1+radius1*Math.sin(arcP3C1X)));
129 | }else if(centerX1centerY2){ //四象限{
130 | arcP3C1X = arcP1C1C2 - arcGradient;
131 | p3.set((float)(centerX1+radius1*Math.cos(arcP3C1X)),(float)(centerY1+radius1*Math.sin(arcP3C1X)));
132 | }else{//二象限
133 | arcP3C1X = arcP1C1C2 - arcGradient;
134 | p3.set((float)(centerX1-radius1*Math.cos(arcP3C1X)),(float)(centerY1-radius1*Math.sin(arcP3C1X)));
135 | }
136 |
137 | // Log.d("edison","斜率: "+arcGradient);
138 | // Log.d("edison","p1 arc: "+arcP1C1X);
139 | // Log.d("edison","p2 arc: "+arcP2C2X);
140 | // Log.d("edison","p3 arc: "+arcP3C1X);
141 | // Log.d("edison","p4 arc: "+arcP4C2Y);
142 |
143 | pointFs[0] = p1;
144 | pointFs[1] = p2;
145 | pointFs[2] = p4;
146 | pointFs[3] = p3;
147 |
148 | return pointFs;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.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 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
21 |
27 |
28 |
37 |
38 |
50 |
51 |
62 |
63 |
74 |
75 |
86 |
87 |
88 |
89 |
96 |
102 |
103 |
109 |
110 |
117 |
118 |
125 |
126 |
133 |
134 |
142 |
143 |
144 |
151 |
159 |
160 |
173 |
174 |
175 |
182 |
188 |
189 |
195 |
196 |
208 |
209 |
214 |
223 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
--------------------------------------------------------------------------------
/magicreddot/src/main/java/com/bupt/edison/magicreddot/MagicRedDotView.java:
--------------------------------------------------------------------------------
1 | package com.bupt.edison.magicreddot;
2 |
3 | import android.animation.Animator;
4 | import android.animation.ValueAnimator;
5 | import android.content.Context;
6 | import android.content.res.TypedArray;
7 | import android.graphics.Canvas;
8 | import android.graphics.Color;
9 | import android.graphics.Paint;
10 | import android.graphics.Path;
11 | import android.graphics.PixelFormat;
12 | import android.graphics.PointF;
13 | import android.graphics.RectF;
14 | import android.graphics.drawable.AnimationDrawable;
15 | import android.text.TextUtils;
16 | import android.util.AttributeSet;
17 | import android.view.Gravity;
18 | import android.view.MotionEvent;
19 | import android.view.View;
20 | import android.view.WindowManager;
21 | import android.view.animation.LinearInterpolator;
22 | import android.view.animation.OvershootInterpolator;
23 | import android.widget.ImageView;
24 |
25 | /**
26 | * Created by edison on 16/6/30.
27 | * 一招退朝
28 | */
29 | public class MagicRedDotView extends View {
30 | Context context;
31 | WindowManager windowManager;
32 | int statusBarHeight;//状态栏高度,在Window中无法测量,需要从Activity中传入
33 |
34 | MagicRedDotView magicRedDotViewInActivity; //Activity中的红点,window中的红点维持一个Activity中红点的引用
35 | MagicRedDotView magicRedDotViewInWindow; //Window中的红点,Acitvity中的红点需要维持一个Window中红点的引用
36 |
37 | Paint dragDotPaint; //红点画笔
38 | Paint rubberPaint;//皮筋画笔
39 | Paint anchorDotPaint;//錨点画笔
40 | Paint messageCountPaint;//未读消息数的画笔
41 |
42 | public static final float bezierCircleConstant = 0.552284749831f; //用于贝塞尔曲线画圆时的常量值
43 | float mDistance; //贝塞尔曲线画圆时的控制线长度, = bezierCircleConstant*radius
44 |
45 | //用六个数据点,八个控制点画红点.n = 4;
46 | PointF upPointFLeft, upPointFRight, downPointFLeft, downPointRight, leftPointF, rightPointF; //数据点
47 | //八个控制点
48 | PointF upLeftPointF, upRightPointF, downLeftPointF, downRightPointF, leftUpPointF, leftDownPointF, rightUpPointF, rightDownPointF; //控制点
49 | Path redDotPath;//红点的贝塞尔曲线path
50 | Path rubberPath;//皮筋的贝塞尔曲线path
51 | PointF anchorPoint;//锚点的圆心点.固定不动的点,记录红点在Activty中初始化时的位置.
52 |
53 | boolean isInitFromLayout = true; //是否从布局文件中初始化,true表示从布局文件中初始化(Activity中的点).false表示从代码中初始化(Window中的点)
54 | int unreadCount = 0; //未读的消息数
55 | RectF dragDotRectF;//红点的范围矩阵,用于判断当前的touch事件是否击中了红点.
56 |
57 | //从xml中读取的红点的属性
58 | float dragDistance;//红点的可拖拽距离,超过这个距离,红点会消失;小于这个距离,红点会复位
59 | float dragDotRadius;//红点的半径
60 | float anchorDotRadius;//锚点的半径.在拖动过程中,它的数值会不断减小.
61 | int dotColor;//红点,皮筋和锚点的颜色
62 | int textColor;//未读消息的字体颜色
63 | int textSize;//未读消息的字体大小
64 | int dotStyle;//红点的style.0,实心点;1,可拖动;2,不可拖动
65 | int countStyle;//未读消息数的显示风格.0,准确显示;1,超过一定数值,就显示一个大概的数
66 | int msgThresholdValue;//消息数量的阈值,当超过这个数,并且显示方式设置为模糊,就开始模糊显示
67 |
68 | float dotRealWidth;//红点真实的宽度
69 | float dotRealHeight;//红点真实的高度
70 | PointF dragDotCenterPoint;//红点的中心点
71 | PointF dragDotLeftTopPoint;//红点的左上角的点
72 | float widgetCenterXInWindow, widgetCenterYInWindow;//控件的中心点在Window中的位置.
73 | float initAnchorRadius;//锚点初始的半径, 与未变化前的anchorDotRadius值相等
74 |
75 |
76 | /**
77 | * 用固定的属性值初始化控件
78 | *
79 | * @param context
80 | */
81 | public MagicRedDotView(Context context) {
82 | super(context);
83 | initAttribute(context);
84 | initTools(context);
85 | isInitFromLayout = false;
86 | windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
87 | }
88 |
89 | public MagicRedDotView(Context context, AttributeSet attrs) {
90 | super(context, attrs);
91 | initAttribute(context, attrs);
92 | initTools(context);
93 | isInitFromLayout = true;
94 | windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
95 | }
96 |
97 | public MagicRedDotView(Context context, AttributeSet attrs, int defStyleAttr) {
98 | super(context, attrs, defStyleAttr);
99 | initAttribute(context, attrs);
100 | initTools(context);
101 | isInitFromLayout = true;
102 | windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
103 | }
104 |
105 | public MagicRedDotView(Context context, WindowManager windowManager, int statusBarHeight,
106 | float widgetCenterXInWindow, float widgetCenterYInWindow,
107 | float dragDistance, float dragDotRadius, float anchorDotRadius,
108 | int dotColor, int textColor, int textSize, int dotStyle, int unreadCount,
109 | int countStyle, int msgThresholdValue) {
110 | super(context);
111 | this.context = context;
112 | this.windowManager = windowManager;
113 | this.statusBarHeight = statusBarHeight;
114 | this.widgetCenterXInWindow = widgetCenterXInWindow;
115 | this.widgetCenterYInWindow = widgetCenterYInWindow;
116 | this.dragDistance = dragDistance;
117 | this.dragDotRadius = dragDotRadius;
118 | this.anchorDotRadius = anchorDotRadius;
119 | this.dotColor = dotColor;
120 | this.textColor = textColor;
121 | this.textSize = textSize;
122 | this.dotStyle = dotStyle;
123 | this.countStyle = countStyle;
124 | this.unreadCount = unreadCount;
125 | this.msgThresholdValue = msgThresholdValue;
126 | initTools(context);
127 | isInitFromLayout = false;
128 | }
129 |
130 | //用固定的属性值初始化各个属性
131 | private void initAttribute(Context context) {
132 | dragDotRadius = Utils.dp2px(context, 20);
133 | anchorDotRadius = Utils.dp2px(context, 16);
134 | dotColor = Color.RED;
135 | textColor = Color.WHITE;
136 | textSize = Utils.sp2px(context, 24);
137 | dotStyle = 2;
138 | countStyle = 1;
139 | msgThresholdValue = 99;
140 | dragDistance = Utils.dp2px(context, 150);
141 | }
142 |
143 | //从layout文件中读取配置,初始化控件
144 | private void initAttribute(Context context, AttributeSet attrs) {
145 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MagicRedDotView);
146 | //红点的半径
147 | dragDotRadius = typedArray.getDimensionPixelOffset(R.styleable.MagicRedDotView_dragDotRadius, 20);
148 | //锚点的半径
149 | anchorDotRadius = typedArray.getDimensionPixelOffset(R.styleable.MagicRedDotView_anchorDotRadius, 16);
150 | //红点的颜色
151 | dotColor = typedArray.getColor(R.styleable.MagicRedDotView_dotColor, Color.RED);
152 | //消息字体的颜色
153 | textColor = typedArray.getColor(R.styleable.MagicRedDotView_textColor, Color.WHITE);
154 | //消息字体的大小
155 | textSize = typedArray.getDimensionPixelSize(R.styleable.MagicRedDotView_textSize, 24);
156 | //红点的风格.0,实心点;1,QQ红点,可拖动;2,普通红点,不可拖动
157 | dotStyle = typedArray.getInt(R.styleable.MagicRedDotView_dotStyle, 2);
158 | //红点可拖动的距离.也是消失距离和复位距离;
159 | dragDistance = typedArray.getDimensionPixelOffset(R.styleable.MagicRedDotView_dragDistance, 150);
160 | //未读消息数的显示风格
161 | countStyle = typedArray.getInt(R.styleable.MagicRedDotView_countStyle, 1);
162 | //未读消息数的阈值
163 | msgThresholdValue = typedArray.getInt(R.styleable.MagicRedDotView_msgThresholdCount, 99);
164 | typedArray.recycle();
165 | }
166 |
167 | private void initTools(Context context) {
168 | this.context = context;
169 |
170 | dragDotPaint = new Paint();
171 | dragDotPaint.setAntiAlias(true);
172 | dragDotPaint.setStyle(Paint.Style.FILL_AND_STROKE);
173 | dragDotPaint.setStrokeWidth(1);
174 | dragDotPaint.setColor(dotColor);
175 |
176 | if (dotStyle == 1 || dotStyle == 2) {
177 | messageCountPaint = new Paint();
178 | messageCountPaint.setColor(textColor);
179 | messageCountPaint.setAntiAlias(true);
180 | messageCountPaint.setStyle(Paint.Style.FILL_AND_STROKE);
181 | messageCountPaint.setStrokeWidth(1);
182 | messageCountPaint.setTextSize(textSize);
183 |
184 | mDistance = bezierCircleConstant * dragDotRadius;
185 |
186 | //六个数据点
187 | upPointFLeft = new PointF();
188 | downPointFLeft = new PointF();
189 | upPointFRight = new PointF();
190 | downPointRight = new PointF();
191 | leftPointF = new PointF();
192 | rightPointF = new PointF();
193 | //八个控制点
194 | upLeftPointF = new PointF();
195 | upRightPointF = new PointF();
196 | downLeftPointF = new PointF();
197 | downRightPointF = new PointF();
198 | leftUpPointF = new PointF();
199 | leftDownPointF = new PointF();
200 | rightUpPointF = new PointF();
201 | rightDownPointF = new PointF();
202 | //绘制红点的贝塞尔曲线
203 | redDotPath = new Path();
204 |
205 | //红点的范围矩阵
206 | dragDotRectF = new RectF();
207 | //红点的中心点
208 | dragDotCenterPoint = new PointF();
209 | //红点的左上角的点
210 | dragDotLeftTopPoint = new PointF();
211 | //锚点的圆心点
212 | anchorPoint = new PointF(); //dotStyle==2时,不需要锚点.但是为了书写方便,还是把它放在这里.
213 | }
214 |
215 | if (dotStyle == 1) {
216 | rubberPaint = new Paint();
217 | rubberPaint.setStrokeWidth(1);
218 | rubberPaint.setColor(dotColor);
219 | rubberPaint.setStyle(Paint.Style.FILL_AND_STROKE);
220 | rubberPaint.setAntiAlias(true);
221 |
222 | anchorDotPaint = new Paint();
223 | anchorDotPaint.setColor(dotColor);
224 | anchorDotPaint.setStrokeWidth(1);
225 | anchorDotPaint.setAntiAlias(true);
226 | anchorDotPaint.setStyle(Paint.Style.FILL_AND_STROKE);
227 |
228 | //绘制皮筋的贝塞尔曲线
229 | rubberPath = new Path();
230 | //初始的锚点半径
231 | initAnchorRadius = anchorDotRadius;
232 | }
233 |
234 | //未读消息的阈值不能小于99
235 | msgThresholdValue = msgThresholdValue < 99 ? 99 : msgThresholdValue;
236 | }
237 |
238 |
239 | @Override
240 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
241 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
242 |
243 | int widthMode = MeasureSpec.getMode(widthMeasureSpec);
244 | int heightMode = MeasureSpec.getMode(heightMeasureSpec);
245 | int widthSize = MeasureSpec.getSize(widthMeasureSpec);
246 | int heightSize = MeasureSpec.getSize(heightMeasureSpec);
247 |
248 | float width, height;
249 |
250 | //红点的高度固定,但是宽度是根据未读消息数而变化的
251 | dotRealHeight = dragDotRadius * 2;
252 | if (dotStyle == 0) { //实心点,没有未读消息数
253 | dotRealWidth = dragDotRadius * 2;
254 | } else { //非实心点,带消息数
255 | //TODO 应该根据countStyle,msgThresholdValue和textSize动态计算红点的宽度
256 | //但是那样有点麻烦,留给下一个版本吧
257 | if (unreadCount >= 0 && unreadCount < 10) { //消息数为个位数
258 | dotRealWidth = dragDotRadius * 2;
259 | } else if (unreadCount >= 10 && unreadCount <= msgThresholdValue) { // 消息数为两位数
260 | dotRealWidth = 12 * dragDotRadius / 5;
261 | } else { //消息数为三位数及以上
262 | dotRealWidth = 3 * dragDotRadius;
263 | }
264 | }
265 |
266 | if (widthMode == MeasureSpec.EXACTLY) {
267 | width = widthSize < dotRealWidth ? dotRealWidth : widthSize; //必须保证可以完整容纳红点
268 | } else {
269 | width = dotRealWidth;
270 | }
271 |
272 | if (heightMode == MeasureSpec.EXACTLY) {
273 | height = heightSize < dragDotRadius * 2 ? dragDotRadius * 2 : heightSize; //必须保证可以完整容纳红点
274 | } else {
275 | height = dragDotRadius * 2;
276 | }
277 |
278 | setMeasuredDimension((int) width + 2, (int) height + 2); //宽高各加两个像素,防止红点的边缘被切掉
279 | }
280 |
281 | @Override
282 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
283 | super.onSizeChanged(w, h, oldw, oldh);
284 |
285 | if (0 != dotStyle) {
286 | //计算红点和锚点的中心点位置,计算红点的数据点和控制点
287 | computePosition();
288 | }
289 | }
290 |
291 | @Override
292 | protected void onDraw(Canvas canvas) {
293 | super.onDraw(canvas);
294 | if (0 == dotStyle) { //实心红点,没有未读消息数
295 | canvas.drawCircle(getWidth() / 2.0f, getHeight() / 2.0f, dragDotRadius, dragDotPaint);
296 | } else if (2 == dotStyle) { //带消息数,但不可拖动
297 | if (unreadCount > 0) {
298 | drawDot(canvas); //只画红点和消息
299 | }
300 | } else { //显示未读消息数
301 | if (unreadCount > 0 && !isDismiss) {
302 | if (isdragable && isInPullScale && isNotExceedPullScale) {
303 | drawRubber(canvas);
304 | drawAnchorDot(canvas);
305 | }
306 | drawDot(canvas); //画红点
307 | }
308 | }
309 | }
310 |
311 | //绘制红点
312 | private void drawDot(Canvas canvas) {
313 | if (unreadCount > 0 && unreadCount <= 9) {
314 | canvas.drawCircle(dragDotCenterPoint.x, dragDotCenterPoint.y, dragDotRadius, dragDotPaint);
315 | } else if (unreadCount > 9) { //用贝塞尔取现画拉伸的红点
316 | redDotPath.reset();
317 | redDotPath.moveTo(upPointFLeft.x, upPointFLeft.y);
318 | redDotPath.lineTo(upPointFRight.x, upPointFRight.y);
319 | redDotPath.cubicTo(upRightPointF.x, upRightPointF.y, rightUpPointF.x, rightUpPointF.y, rightPointF.x, rightPointF.y);
320 | redDotPath.cubicTo(rightDownPointF.x, rightDownPointF.y, downRightPointF.x, downRightPointF.y, downPointRight.x, downPointRight.y);
321 | redDotPath.lineTo(downPointFLeft.x, downPointFLeft.y);
322 | redDotPath.cubicTo(downLeftPointF.x, downLeftPointF.y, leftDownPointF.x, leftDownPointF.y, leftPointF.x, leftPointF.y);
323 | redDotPath.cubicTo(leftUpPointF.x, leftUpPointF.y, upLeftPointF.x, upLeftPointF.y, upPointFLeft.x, upPointFLeft.y);
324 | canvas.drawPath(redDotPath, dragDotPaint);
325 | }
326 |
327 | drawMsgCount(canvas);
328 | }
329 |
330 | //绘制红点中的消息数量文字
331 | private void drawMsgCount(Canvas canvas) {
332 | String count = "";
333 | if (unreadCount > 0 && unreadCount <= msgThresholdValue) {
334 | count = String.valueOf(unreadCount);
335 | } else if (unreadCount > msgThresholdValue) {
336 | if (0 == countStyle) { //准确显示数目
337 | count = String.valueOf(unreadCount);
338 | } else { //模糊显示
339 | count = String.valueOf(msgThresholdValue) + "+";
340 | }
341 | }
342 | if (!TextUtils.isEmpty(count)) {
343 | int countWidth = Utils.computeStringWidth(messageCountPaint, count);
344 | int countHeight = Utils.computeStringHeight(messageCountPaint, count);
345 | canvas.drawText(count, dragDotCenterPoint.x - countWidth / 2, dragDotCenterPoint.y + countHeight / 2, messageCountPaint);
346 | }
347 | }
348 |
349 | /**
350 | * 拖拽时,绘制一个锚点
351 | *
352 | * @param canvas
353 | */
354 | private void drawAnchorDot(Canvas canvas) {
355 | canvas.drawCircle(anchorPoint.x, anchorPoint.y, anchorDotRadius, anchorDotPaint);
356 | }
357 |
358 | /**
359 | * 拖拽时,绘制一条橡皮筋,连接红点与锚点
360 | *
361 | * @param canvas
362 | */
363 | private void drawRubber(Canvas canvas) {
364 | PointF[] pointFs = MathUtils.getTangentPoint(anchorPoint.x, anchorPoint.y, anchorDotRadius, moveX, moveY, dragDotRadius);
365 | PointF controlPointF = MathUtils.getMiddlePoint(anchorPoint.x, anchorPoint.y, moveX, moveY);
366 | //利用贝塞尔取现画出皮筋
367 | rubberPath.reset();
368 | rubberPath.moveTo(anchorPoint.x, anchorPoint.y);
369 | rubberPath.lineTo(pointFs[0].x, pointFs[0].y);
370 | rubberPath.quadTo(controlPointF.x, controlPointF.y, pointFs[1].x, pointFs[1].y);
371 | rubberPath.lineTo(moveX, moveY);
372 | rubberPath.lineTo(pointFs[2].x, pointFs[2].y);
373 | rubberPath.quadTo(controlPointF.x, controlPointF.y, pointFs[3].x, pointFs[3].y);
374 | rubberPath.lineTo(anchorPoint.x, anchorPoint.y);
375 |
376 | canvas.drawPath(rubberPath, rubberPaint);
377 | }
378 |
379 | float downX, downY, moveX, moveY, upX, upY;
380 | boolean isdragable = false;//是否可拖拽
381 | boolean isInPullScale = true; //是否在拉力范围内
382 | boolean isDismiss = false;//是否应该dismiss小红点
383 | boolean isNotExceedPullScale = true;//是否未曾脱离拉力范围.false表示已经至少脱离过拉力范围一次;true表示尚未脱离过拉力范围
384 |
385 | @Override
386 | public boolean onTouchEvent(MotionEvent event) {
387 | if (1 != dotStyle) { //只有dotStyle=1时,才可以拖动
388 | return super.onTouchEvent(event);
389 | }
390 | switch (event.getAction()) {
391 | case MotionEvent.ACTION_DOWN:
392 | if (!isInitFromLayout) { //从代码中实例化View
393 | downX = event.getRawX();
394 | downY = event.getRawY() - getStatusBarHeight(); //校正坐标
395 |
396 | //在这里,dragDotRectF还没有被赋值
397 | isdragable = false;
398 | } else { //从xml中实例化View
399 | downX = event.getX();
400 | downY = event.getY();
401 | if (dragDotRectF.contains(downX, downY)) { //击中了红点,允许拖动
402 | isdragable = true;
403 | //在这里会遇到一个坑,getLocationInWindow返回的坐标与getLocationOnScreen返回的坐标相同
404 | //这会导致计算错误,引起后续一系列问题.Android坑真多,被这个问题困了很久.参考下面两个答案
405 | //http://stackoverflow.com/questions/17672891/getlocationonscreen-vs-getlocationinwindow
406 | //http://stackoverflow.com/questions/2638342/incorrect-coordinates-from-getlocationonscreen-getlocationinwindow
407 | int[] locationInScreen = new int[2]; //控件在当前屏幕中的坐标
408 | getLocationOnScreen(locationInScreen);
409 | setStatusBarHeight(Utils.getStatusBarHeight(this));
410 | magicRedDotViewInWindow = new MagicRedDotView.Builder().setAnchorDotRadius(this.anchorDotRadius)
411 | .setDotColor(this.dotColor).setDotStyle(this.dotStyle).setDragDotRadius(this.dragDotRadius)
412 | .setWindowManager(this.windowManager).setContext(this.context).setCountStyle(this.countStyle)
413 | .setDragDistance(this.dragDistance).setUnreadCount(this.unreadCount).setTextSize(this.textSize)
414 | .setTextColor(this.textColor).setStatusBarHeight(this.statusBarHeight).setMsgThresholdValue(this.msgThresholdValue)
415 | .setWidgetCenterXInWindow(locationInScreen[0] + getWidth() / 2f)
416 | .setwidgetCenterYInWindow(locationInScreen[1] + getHeight() / 2f - getStatusBarHeight())
417 | .create();
418 | magicRedDotViewInWindow.setMagicRedDotViewInActivity(this);
419 |
420 | addMagicRedDotViewToWindow(magicRedDotViewInWindow); //添加到window中
421 | this.setVisibility(GONE);//隐藏Activity中的红点
422 |
423 | //红点开始拖动时的监听
424 | if (null != onDragStartListener) {
425 | onDragStartListener.OnDragStart();
426 | }
427 | }
428 | }
429 | break;
430 | case MotionEvent.ACTION_MOVE:
431 | if (!isInitFromLayout) {
432 | //拖动的时候,再允许红点拉伸
433 | isdragable = true;
434 |
435 | moveX = event.getRawX();
436 | moveY = event.getRawY() - getStatusBarHeight();//把坐标校正成Window中坐标
437 | if (MathUtils.getDistanceBetweenPoints(moveX, moveY, anchorPoint.x, anchorPoint.y) <= dragDistance) {
438 | isInPullScale = true;
439 | updateAnchorDotRadius(moveX, moveY);
440 | } else {
441 | isNotExceedPullScale = false;
442 | isInPullScale = false;
443 | }
444 | computePosition(moveX, moveY);
445 | invalidate();
446 | }
447 | break;
448 | case MotionEvent.ACTION_UP:
449 | if (!isInitFromLayout) {
450 | if (isdragable && isInPullScale) { //在拉力范围内,要使红点复位
451 | upX = event.getRawX();
452 | upY = event.getRawY() - getStatusBarHeight();//校正坐标
453 | if (isNotExceedPullScale) { //未曾脱离过拉力范围(橡皮筋还没有断),复位时,要有一个橡皮筋效果
454 | animatorBackToAnchorPoint(upX, upY);
455 | } else { //曾经脱离过拉力范围(橡皮筋已断),复位时,直接回到原始点
456 | simpleBackToAnchorPoint(upX, upY);
457 | }
458 | } else if (isdragable && !isInPullScale) { //超过拉力范围,播放消失动画
459 | upX = event.getRawX();
460 | upY = event.getRawY() - getStatusBarHeight();//校正坐标
461 |
462 | //消失
463 | isDismiss = true;
464 | invalidate();
465 | animationDismiss(upX, upY);
466 |
467 | //红点消失时的监听
468 | if (null != getMagicRedDotViewInActivity().getOnDotDismissListener()) {
469 | getMagicRedDotViewInActivity().getOnDotDismissListener().OnDotDismiss();
470 | }
471 | }
472 | }
473 | break;
474 | default:
475 | break;
476 | }
477 | //虽然magicRedDotViewInActivity会被隐藏,但是所有的Touch事件仍会被传递给
478 | //magicRedDotViewInActivity,所以需要在magicRedDotViewInActivity的onTouchEvent()函数中
479 | //把Touch事件传给magicRedDotViewInWindow
480 | if (magicRedDotViewInWindow != null) {
481 | magicRedDotViewInWindow.dispatchTouchEvent(event);
482 | }
483 | return true;
484 | }
485 |
486 | //拖拽过程中更新锚点半径.拖拽时,锚点的半径会逐渐变小.
487 | private void updateAnchorDotRadius(float moveX, float moveY) {
488 | float distance = MathUtils.getDistanceBetweenPoints(moveX, moveY, anchorPoint.x, anchorPoint.y);
489 | anchorDotRadius = initAnchorRadius - (distance / dragDistance) * (initAnchorRadius - 5);
490 | }
491 |
492 | /**
493 | * 获取控件的中心点在window中的坐标
494 | *
495 | * @return
496 | */
497 | public float getWidgetCenterXInWindow() {
498 | return widgetCenterXInWindow;
499 | }
500 |
501 | /**
502 | * 获取控件的中心点在window中的坐标
503 | *
504 | * @return
505 | */
506 | public float getWidgetCenterYInWindow() {
507 | return widgetCenterYInWindow;
508 | }
509 |
510 | /**
511 | * 计算红点数据点和控制点
512 | * 红点位于控件的中心点位置,红点的中心点位置与控件的中心点位置重合.
513 | */
514 | private void computePosition() {
515 | if (isInitFromLayout) {
516 | anchorPoint.set(getWidth() / 2.0f, getHeight() / 2.0f);//保存锚点的位置
517 | computePosition(getWidth() / 2.0f, getHeight() / 2.0f);
518 | } else {
519 | anchorPoint.set(getWidgetCenterXInWindow(), getWidgetCenterYInWindow());//保存锚点的位置,因为锚点的位置是固定不变的,所以不能随着的touch事件更新,故放在这里初始化
520 | computePosition(getWidgetCenterXInWindow(), getWidgetCenterYInWindow());
521 | }
522 | }
523 |
524 | /**
525 | * 根据中心点的位置,计算红点的数据点和控制点
526 | *
527 | * @param centerX
528 | * @param centerY
529 | */
530 | private void computePosition(float centerX, float centerY) {
531 | dragDotCenterPoint.set(centerX, centerY);//保存中心点位置
532 | dragDotLeftTopPoint = center2LeftTop(dragDotCenterPoint);//保存左上角的位置
533 | //TODO 根据countStyle和msgThresholdValue
534 | if (unreadCount > 0 && unreadCount <= 9) {
535 | dragDotRectF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y, dragDotLeftTopPoint.x + dragDotRadius * 2, dragDotLeftTopPoint.y + dragDotRadius * 2);
536 | } else if (unreadCount > 9 && unreadCount <= msgThresholdValue) {
537 | dragDotRectF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y, dragDotLeftTopPoint.x + dragDotRadius * 12 / 5, dragDotLeftTopPoint.y + dragDotRadius * 2);
538 | computeRedDotBezierPoint(12 * dragDotRadius / 5, 2 * dragDotRadius);
539 | } else if (unreadCount > msgThresholdValue) {
540 | dragDotRectF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y, dragDotLeftTopPoint.x + dragDotRadius * 3, dragDotLeftTopPoint.y + dragDotRadius * 2);
541 | computeRedDotBezierPoint(3 * dragDotRadius, 2 * dragDotRadius);
542 | }
543 | }
544 |
545 | /**
546 | * 计算红点的数据点和控制点
547 | *
548 | * @param width 红点的实际宽度
549 | * @param height 红点的实际高度
550 | */
551 | private void computeRedDotBezierPoint(float width, float height) {
552 | //数据点
553 | upPointFLeft.set(dragDotLeftTopPoint.x + dragDotRadius, dragDotLeftTopPoint.y);
554 | leftPointF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y + dragDotRadius);
555 | downPointFLeft.set(dragDotLeftTopPoint.x + dragDotRadius, dragDotLeftTopPoint.y + height);
556 |
557 | upPointFRight.set(dragDotLeftTopPoint.x + width - dragDotRadius, dragDotLeftTopPoint.y);
558 | rightPointF.set(dragDotLeftTopPoint.x + width, dragDotLeftTopPoint.y + dragDotRadius);
559 | downPointRight.set(dragDotLeftTopPoint.x + width - dragDotRadius, dragDotLeftTopPoint.y + height);
560 |
561 | //控制点
562 | upLeftPointF.set(dragDotLeftTopPoint.x + dragDotRadius - mDistance, dragDotLeftTopPoint.y);
563 | upRightPointF.set(dragDotLeftTopPoint.x + width - dragDotRadius + mDistance, dragDotLeftTopPoint.y);
564 | downLeftPointF.set(dragDotLeftTopPoint.x + dragDotRadius - mDistance, dragDotLeftTopPoint.y + height);
565 | downRightPointF.set(dragDotLeftTopPoint.x + width - dragDotRadius + mDistance, dragDotLeftTopPoint.y + height);
566 | leftUpPointF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y + dragDotRadius - mDistance);
567 | leftDownPointF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y + dragDotRadius + mDistance);
568 | rightUpPointF.set(dragDotLeftTopPoint.x + width, dragDotLeftTopPoint.y + dragDotRadius - mDistance);
569 | rightDownPointF.set(dragDotLeftTopPoint.x + width, dragDotLeftTopPoint.y + dragDotRadius + mDistance);
570 | }
571 |
572 | /**
573 | * 根据中间点的坐标,计算出红点实际左上角点的坐标
574 | * 注意,不是控件左上角.控件是大于等于红点的
575 | *
576 | * @param centerPointF
577 | * @return
578 | */
579 | private PointF center2LeftTop(PointF centerPointF) {
580 | PointF leftTopPointF = new PointF();
581 | if (unreadCount >= 0 && unreadCount < 10) {
582 | leftTopPointF.set(centerPointF.x - dragDotRadius, centerPointF.y - dragDotRadius);
583 | } else if (unreadCount >= 10 && unreadCount <= msgThresholdValue) {
584 | leftTopPointF.set(centerPointF.x - 6 * dragDotRadius / 5, centerPointF.y - dragDotRadius);
585 | } else {
586 | //TODO 这里需要根据countStyle的值定制
587 | leftTopPointF.set(centerPointF.x - 3 * dragDotRadius / 2, centerPointF.y - dragDotRadius);
588 | }
589 | return leftTopPointF;
590 | }
591 |
592 | //红点中心点坐标转换为左上角坐标
593 | private float centerX2StartX(float centerX) {
594 | float startX;
595 | if (unreadCount >= 0 && unreadCount < 10) {
596 | startX = centerX - dragDotRadius;
597 | } else if (unreadCount >= 10 && unreadCount <= msgThresholdValue) {
598 | startX = centerX - 6 * dragDotRadius / 5;
599 | } else {
600 | startX = centerX - 3 * dragDotRadius / 2;
601 | }
602 | return startX;
603 | }
604 |
605 | //红点中心点坐标转换为左上角坐标
606 | private float centerY2StartY(float centerY) {
607 | return centerY - dragDotRadius;
608 | }
609 |
610 | //小红点的消失动画
611 | private void animationDismiss(float upX, float upY) {
612 | final ImageView imageView = new ImageView(context);
613 | imageView.setImageResource(R.drawable.dismiss_anim);
614 | final AnimationDrawable animationDrawable = (AnimationDrawable) imageView.getDrawable();
615 | long duration = 500;
616 | int width = imageView.getDrawable().getIntrinsicWidth();
617 | int height = imageView.getDrawable().getIntrinsicHeight();
618 | WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
619 | layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
620 | layoutParams.format = PixelFormat.RGBA_8888;
621 | layoutParams.x = (int) (upX - width / 2);
622 | layoutParams.y = (int) (upY - height / 2);
623 | layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
624 | layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
625 | windowManager.addView(imageView, layoutParams);
626 | animationDrawable.start();
627 | imageView.postDelayed(new Runnable() {
628 | @Override
629 | public void run() {
630 | //更新未读消息数为0
631 | getMagicRedDotViewInActivity().setUnreadCount(0);
632 |
633 | animationDrawable.stop();
634 | imageView.clearAnimation();
635 | windowManager.removeView(imageView);
636 | removeMagicRedDotViewToWindow();
637 | }
638 | }, duration);
639 | }
640 |
641 | //简单的复位动画,没有回弹效果.
642 | private void simpleBackToAnchorPoint(final float upX, final float upY) {
643 | ValueAnimator animatorX = ValueAnimator.ofFloat(upX, anchorPoint.x);
644 | animatorX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
645 | @Override
646 | public void onAnimationUpdate(ValueAnimator animation) {
647 | float fraction = animation.getAnimatedFraction();
648 | float currentX = (float) animation.getAnimatedValue();
649 | float currentY = (anchorPoint.y - upY) * fraction + upY;
650 | moveX = currentX; //这时皮筋已断,已经不会再绘制皮筋了,所以其实可以取消对moveX和moveY的赋值操作
651 | moveY = currentY;
652 | computePosition(currentX, currentY);
653 | invalidate();
654 | }
655 | });
656 | animatorX.addListener(animatorListener);
657 | //不需要回弹效果,直接使用线性插值器
658 | animatorX.setInterpolator(new LinearInterpolator());
659 | animatorX.setDuration(200);
660 | animatorX.start();
661 | }
662 |
663 | //回到初始位置,带有回弹效果
664 | private void animatorBackToAnchorPoint(final float upX, final float upY) {
665 | ValueAnimator animatorX = ValueAnimator.ofFloat(upX, anchorPoint.x);
666 | animatorX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
667 | @Override
668 | public void onAnimationUpdate(ValueAnimator animation) {
669 | float fraction = animation.getAnimatedFraction();
670 | float currentX = (float) animation.getAnimatedValue();
671 | float currentY = (anchorPoint.y - upY) * fraction + upY;
672 | moveX = currentX; //画皮筋时要用到moveX和moveY
673 | moveY = currentY;
674 | computePosition(currentX, currentY);
675 | invalidate();
676 | }
677 | });
678 | animatorX.addListener(animatorListener);
679 | //TODO 这个回弹效果不够机智,将来自顶一个Interpolator优化一下
680 | animatorX.setInterpolator(new OvershootInterpolator(4.0f));
681 | //animatorX.setInterpolator(new BounceInterpolator()); //这个回弹效果不太好用
682 | animatorX.setDuration(500);
683 | animatorX.start();
684 | }
685 |
686 | //红点复位动画的监听器
687 | Animator.AnimatorListener animatorListener = new Animator.AnimatorListener() {
688 | @Override
689 | public void onAnimationStart(Animator animation) {
690 |
691 | }
692 |
693 | @Override
694 | public void onAnimationEnd(Animator animation) {
695 | resetStatus();
696 | getMagicRedDotViewInActivity().setVisibility(VISIBLE);
697 | getMagicRedDotViewInActivity().resetStatus();
698 |
699 | //红点复位时的监听
700 | if (null != getMagicRedDotViewInActivity().getOnDotResetListener()) {
701 | getMagicRedDotViewInActivity().getOnDotResetListener().OnDotReset();
702 | }
703 |
704 | removeMagicRedDotViewToWindow();
705 | }
706 |
707 | @Override
708 | public void onAnimationCancel(Animator animation) {
709 |
710 | }
711 |
712 | @Override
713 | public void onAnimationRepeat(Animator animation) {
714 |
715 | }
716 | };
717 |
718 | //重置状态值
719 | public void resetStatus() {
720 | isdragable = false;
721 | isInPullScale = true;
722 | isNotExceedPullScale = true;
723 | isDismiss = false;
724 | }
725 |
726 | //添加红点到WindowManager
727 | public void addMagicRedDotViewToWindow(MagicRedDotView magicRedDotView) {
728 | WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
729 | layoutParams.format = PixelFormat.RGBA_8888;
730 | // layoutParams.format = PixelFormat.TRANSLUCENT;
731 | layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
732 | layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
733 | layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;
734 | layoutParams.x = 0;
735 | layoutParams.y = 0;
736 | windowManager.addView(magicRedDotView, layoutParams);
737 | }
738 |
739 | //从Window中把红点移除
740 | public void removeMagicRedDotViewToWindow() {
741 | windowManager.removeView(this);
742 | }
743 |
744 | /**
745 | * 保存对Activity中红点的引用
746 | *
747 | * @param magicRedDotView
748 | */
749 | public void setMagicRedDotViewInActivity(MagicRedDotView magicRedDotView) {
750 | this.magicRedDotViewInActivity = magicRedDotView;
751 | }
752 |
753 | /**
754 | * 获取Activity中的红点对象的引用
755 | *
756 | * @return
757 | */
758 | public MagicRedDotView getMagicRedDotViewInActivity() {
759 | return this.magicRedDotViewInActivity;
760 | }
761 |
762 | /**
763 | * 获取状态栏的高度
764 | *
765 | * @return
766 | */
767 | public int getStatusBarHeight() {
768 | return statusBarHeight;
769 | }
770 |
771 | /**
772 | * 设置状态栏的高度
773 | *
774 | * @param statusBarHeight
775 | */
776 | public void setStatusBarHeight(int statusBarHeight) {
777 | this.statusBarHeight = statusBarHeight;
778 | }
779 |
780 | /**
781 | * 更新未读消息的数量
782 | * 参考:http://blog.csdn.net/yanbober/article/details/46128379
783 | */
784 | public void setUnreadCount(int unreadCount) {
785 | int lastCount = this.unreadCount;
786 | this.unreadCount = unreadCount;
787 | if (0 == lastCount) {
788 | requestLayout();
789 | invalidate();
790 | } else if (lastCount > 0 && lastCount < 10) {
791 | if (unreadCount < 10) {
792 | invalidate();
793 | } else {
794 | requestLayout();
795 | invalidate();
796 | }
797 | } else if (lastCount >= 10 && lastCount <= msgThresholdValue) {
798 | if (unreadCount < 10) {
799 | requestLayout();
800 | invalidate();
801 | } else if (unreadCount >= 10 && unreadCount <= msgThresholdValue) {
802 | invalidate();
803 | } else if (unreadCount > msgThresholdValue) {
804 | requestLayout();
805 | invalidate();
806 | }
807 | } else if (lastCount > msgThresholdValue) {
808 | if (unreadCount > msgThresholdValue) {
809 | invalidate();
810 | } else {
811 | requestLayout();
812 | invalidate();
813 | }
814 | }
815 | }
816 |
817 | /**
818 | * 返回当前未读消息数量
819 | *
820 | * @return
821 | */
822 | public int getUnreadCount() {
823 | return unreadCount;
824 | }
825 |
826 | /**
827 | * 开始拖动红点的监听
828 | */
829 | public interface OnDragStartListener {
830 | void OnDragStart();
831 | }
832 |
833 | OnDragStartListener onDragStartListener;
834 |
835 | public void setOnDragStartListener(OnDragStartListener onDragStartListener) {
836 | this.onDragStartListener = onDragStartListener;
837 | }
838 |
839 | public OnDragStartListener getOnDragStartListener() {
840 | return onDragStartListener;
841 | }
842 |
843 | /**
844 | * 红点消失的监听
845 | */
846 | public interface OnDotDismissListener {
847 | void OnDotDismiss();
848 | }
849 |
850 | OnDotDismissListener onDotDismissListener;
851 |
852 | public void setOnDotDismissListener(OnDotDismissListener onDotDismissListener) {
853 | this.onDotDismissListener = onDotDismissListener;
854 | }
855 |
856 | public OnDotDismissListener getOnDotDismissListener() {
857 | return onDotDismissListener;
858 | }
859 |
860 | /**
861 | * 红点复位时的监听
862 | */
863 | public interface OnDotResetListener {
864 | void OnDotReset();
865 | }
866 |
867 | OnDotResetListener onDotResetListener;
868 |
869 | public void setOnDotResetListener(OnDotResetListener onDotResetListener) {
870 | this.onDotResetListener = onDotResetListener;
871 | }
872 |
873 | public OnDotResetListener getOnDotResetListener() {
874 | return onDotResetListener;
875 | }
876 |
877 | /**
878 | * 使用builder模式初始化控件
879 | * 从代码中初始化控件时调用
880 | */
881 | public static class Builder {
882 | private Context context;
883 | private WindowManager windowManager;//在window中
884 | private int statusBarHeight;//状态栏高度,在Window中无法测量,需要从Activity中传入
885 | private float widgetCenterXInWindow;//控件的中心点在Window中的位置.
886 | private float widgetCenterYInWindow;//控件的中心点在Window中的位置.
887 |
888 | private float dragDistance;//红点的可拖拽距离,超过这个距离,红点会消失;小于这个距离,红点会复位
889 | private float dragDotRadius;//红点的半径
890 | private float anchorDotRadius;//锚点的半径.在拖动过程中,它的数值会不断减小.
891 | private int dotColor;//红点,皮筋和锚点的颜色
892 | private int textColor;//未读消息的字体颜色
893 | private int textSize;//未读消息的字体大小
894 | private int dotStyle;//红点的style.0,实心点;1,可拖动;2,不可拖动
895 | private int countStyle;//未读消息数的显示风格.0,准确显示;1,超过一定数值,就显示一个大概的数
896 | private int msgThresholdValue;//未读消息数的阈值
897 |
898 | private int unreadCount;//未读消息数
899 |
900 | public Builder() {
901 | }
902 |
903 | public Builder setAnchorDotRadius(float anchorDotRadius) {
904 | this.anchorDotRadius = anchorDotRadius;
905 | return this;
906 | }
907 |
908 | public Builder setContext(Context context) {
909 | this.context = context;
910 | return this;
911 | }
912 |
913 | public Builder setDotColor(int dotColor) {
914 | this.dotColor = dotColor;
915 | return this;
916 | }
917 |
918 | public Builder setDotStyle(int dotStyle) {
919 | this.dotStyle = dotStyle;
920 | return this;
921 | }
922 |
923 | public Builder setDragDistance(float dragDistance) {
924 | this.dragDistance = dragDistance;
925 | return this;
926 | }
927 |
928 | public Builder setDragDotRadius(float dragDotRadius) {
929 | this.dragDotRadius = dragDotRadius;
930 | return this;
931 | }
932 |
933 | public Builder setStatusBarHeight(int statusBarHeight) {
934 | this.statusBarHeight = statusBarHeight;
935 | return this;
936 | }
937 |
938 | public Builder setWidgetCenterXInWindow(float widgetCenterXInWindow) {
939 | this.widgetCenterXInWindow = widgetCenterXInWindow;
940 | return this;
941 | }
942 |
943 | public Builder setwidgetCenterYInWindow(float widgetCenterYInWindow) {
944 | this.widgetCenterYInWindow = widgetCenterYInWindow;
945 | return this;
946 | }
947 |
948 | public Builder setTextColor(int textColor) {
949 | this.textColor = textColor;
950 | return this;
951 | }
952 |
953 | public Builder setTextSize(int textSize) {
954 | this.textSize = textSize;
955 | return this;
956 | }
957 |
958 | public Builder setMsgThresholdValue(int msgThresholdValue) {
959 | this.msgThresholdValue = msgThresholdValue;
960 | return this;
961 | }
962 |
963 | public Builder setWindowManager(WindowManager windowManager) {
964 | this.windowManager = windowManager;
965 | return this;
966 | }
967 |
968 | public Builder setUnreadCount(int unreadCount) {
969 | this.unreadCount = unreadCount;
970 | return this;
971 | }
972 |
973 | public Builder setCountStyle(int countStyle) {
974 | this.countStyle = countStyle;
975 | return this;
976 | }
977 |
978 | public MagicRedDotView create() {
979 | return new MagicRedDotView(context, windowManager, statusBarHeight,
980 | widgetCenterXInWindow, widgetCenterYInWindow,
981 | dragDistance, dragDotRadius, anchorDotRadius, dotColor, textColor, textSize,
982 | dotStyle, unreadCount, countStyle, msgThresholdValue);
983 | }
984 | }
985 | }
986 |
--------------------------------------------------------------------------------