├── sample
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable-xhdpi
│ │ │ └── bubble.png
│ │ ├── 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
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ └── styles.xml
│ │ ├── values-v21
│ │ │ └── styles.xml
│ │ ├── values-w820dp
│ │ │ └── dimens.xml
│ │ ├── menu
│ │ │ └── menu_main.xml
│ │ └── layout
│ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── wj
│ │ └── sticky
│ │ └── demo
│ │ └── MainActivity.java
├── proguard-rules.pro
├── build.gradle
└── sample.iml
├── StickyFlagView
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ └── attrs.xml
│ │ └── drawable-xhdpi
│ │ │ ├── disappear0.png
│ │ │ ├── disappear1.png
│ │ │ ├── disappear2.png
│ │ │ ├── disappear3.png
│ │ │ └── disappear4.png
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── wj
│ │ ├── utils
│ │ ├── DrawUtils.java
│ │ └── ScreenUtils.java
│ │ └── sticky
│ │ └── StickyFlagView.java
├── proguard-rules.pro
├── build.gradle
└── StickyFlagView-StickyFlagView.iml
├── settings.gradle
├── sticky.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── README.md
├── gradlew.bat
└── gradlew
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/StickyFlagView/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample', ':StickyFlagView'
2 |
--------------------------------------------------------------------------------
/sticky.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/sticky.gif
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/StickyFlagView/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | StickyFlagView
3 |
4 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xhdpi/bubble.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/sample/src/main/res/drawable-xhdpi/bubble.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/StickyFlagView/src/main/res/drawable-xhdpi/disappear0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/StickyFlagView/src/main/res/drawable-xhdpi/disappear0.png
--------------------------------------------------------------------------------
/StickyFlagView/src/main/res/drawable-xhdpi/disappear1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/StickyFlagView/src/main/res/drawable-xhdpi/disappear1.png
--------------------------------------------------------------------------------
/StickyFlagView/src/main/res/drawable-xhdpi/disappear2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/StickyFlagView/src/main/res/drawable-xhdpi/disappear2.png
--------------------------------------------------------------------------------
/StickyFlagView/src/main/res/drawable-xhdpi/disappear3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/StickyFlagView/src/main/res/drawable-xhdpi/disappear3.png
--------------------------------------------------------------------------------
/StickyFlagView/src/main/res/drawable-xhdpi/disappear4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijia1991/StickyFlagView/HEAD/StickyFlagView/src/main/res/drawable-xhdpi/disappear4.png
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | StickyFlagView
3 | Settings
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 28 10:00:20 PST 2015
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.10-all.zip
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 |
7 |
--------------------------------------------------------------------------------
/StickyFlagView/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/sample/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/sample/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 |
15 | # Gradle files
16 | .gradle/
17 | build/
18 |
19 | # Local configuration file (sdk path, etc)
20 | local.properties
21 |
22 | # Proguard folder generated by Eclipse
23 | proguard/
24 |
25 | # Log Files
26 | *.log
27 |
28 | # Android Studio Navigation editor temp files
29 | .navigation/
30 |
31 | # Android Studio captures folder
32 | captures/
33 |
--------------------------------------------------------------------------------
/StickyFlagView/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/sample/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 D:\DevelopSoftware\android_sdk_studio/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 |
--------------------------------------------------------------------------------
/StickyFlagView/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 D:\DevelopSoftware\android_sdk_studio/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 |
--------------------------------------------------------------------------------
/StickyFlagView/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 'Google Inc.:Google APIs:23'
5 | buildToolsVersion "23.0.2"
6 |
7 | defaultConfig {
8 | minSdkVersion 9
9 | targetSdkVersion 23
10 | versionCode 1
11 | versionName "1.0"
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled false
16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 | }
20 |
21 | dependencies {
22 | compile fileTree(dir: 'libs', include: ['*.jar'])
23 | testCompile 'junit:junit:4.12'
24 | compile 'com.android.support:appcompat-v7:23.+'
25 | compile 'com.nineoldandroids:library:2.4.0'
26 | }
27 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 'Google Inc.:Google APIs:23'
5 | buildToolsVersion "23.0.2"
6 |
7 | defaultConfig {
8 | applicationId "com.wj.sticky.demo"
9 | minSdkVersion 9
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(dir: 'libs', include: ['*.jar'])
24 | testCompile 'junit:junit:4.12'
25 | compile 'com.android.support:appcompat-v7:23.+'
26 | compile 'com.android.support:design:23.+'
27 | compile project(':StickyFlagView')
28 | }
29 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # StickyFlagView
2 | StickyFlagView is an unread message tag which can drag.
3 | 
4 | # Usage
5 | * In XML:
6 | 1.default flag view
7 | `
8 | ` android:layout_width="40dp"`
9 | ` android:layout_height="40dp"`
10 | ` app:flagRadius="10dp" />`
11 | 2.flag view with drawable resource
12 | `
13 | ` android:layout_width="wrap_content"`
14 | ` android:layout_height="wrap_content"`
15 | ` app:flagDrawable="@drawable/bubble"`
16 | ` app:flagColor="#f74c31" />`
17 | * In code:
18 | `StickyFlagView sfv = new StickyFlagView(MainActivity.this);`
19 | `sfv.setFlagText("9");`
20 | `sfv.setFlagTextColor(Color.WHITE);`
21 | `sfv.setFlagTextSize(15);`
22 | `sfv.setFlagColor(Color.RED);`
23 | `sfv.setFlagRadius(10);`
24 | `sfv.setMinStickRadius(3);`
25 | `sfv.setMaxStickRadius(8);`
26 | `sfv.setMaxDistance(80);`
27 | `sfv.setOnFlagDisappearListener(new StickyFlagView.OnFlagDisappearListener() {`
28 | ` @Override`
29 | ` public void onFlagDisappear(StickyFlagView view) {`
30 | ` }`
31 | `});`
32 |
--------------------------------------------------------------------------------
/StickyFlagView/src/main/java/com/wj/utils/DrawUtils.java:
--------------------------------------------------------------------------------
1 | package com.wj.utils;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Paint;
5 |
6 | /**
7 | * 绘图工具类
8 | * Created by jia.wei on 15/7/29.
9 | */
10 | public class DrawUtils {
11 |
12 | /**
13 | * 测量文本内容宽度
14 | *
15 | * @param text 文本内容
16 | * @param paint 画笔
17 | * @return 内容宽度
18 | */
19 | public static float measureTextWidth(String text, Paint paint) {
20 | float width = paint.measureText(text);
21 |
22 | return width;
23 | }
24 |
25 | /**
26 | * 测量文本内容高度
27 | *
28 | * @param paint 画笔
29 | * @return 内容高度
30 | */
31 | public static float measureTextHeight(Paint paint) {
32 | Paint.FontMetrics fm = paint.getFontMetrics();
33 | float height = fm.bottom - fm.top;
34 |
35 | return height;
36 | }
37 |
38 | /**
39 | * 文本内容相对于centerX点居中
40 | *
41 | * @param canvas 画布
42 | * @param paint 画笔
43 | * @param text 文本内容
44 | * @param centerX x轴方向中心点
45 | * @param y 文本内容y轴方向偏移量
46 | */
47 | public static void drawTextByCenterX(Canvas canvas, Paint paint, String text, float centerX, float y) {
48 | canvas.drawText(text, (float) (centerX - measureTextWidth(text, paint) * 0.5), y, paint);
49 | }
50 |
51 | /**
52 | * 文本内容相对于centerY点居中
53 | *
54 | * @param canvas 画布
55 | * @param paint 画笔
56 | * @param text 文本内容
57 | * @param x 文本内容x轴方向偏移量
58 | * @param centerY y轴方向中心点
59 | */
60 | public static void drawTextByCenterY(Canvas canvas, Paint paint, String text, float x, float centerY) {
61 | float bottom = paint.getFontMetrics().bottom;
62 | canvas.drawText(text, x, (float) (0.5 * measureTextHeight(paint) - bottom + centerY), paint);
63 | }
64 |
65 | /**
66 | * 文本内容相对于某一点居中
67 | *
68 | * @param canvas 画布
69 | * @param paint 画笔
70 | * @param text 文本内容
71 | * @param centerX x轴方向中心点
72 | * @param centerY y轴方向中心点
73 | */
74 | public static void drawTextInCenter(Canvas canvas, Paint paint, String text, float centerX, float centerY) {
75 | float bottom = paint.getFontMetrics().bottom;
76 | canvas.drawText(text, (float) (centerX - measureTextWidth(text, paint) * 0.5),
77 | (float) (0.5 * measureTextHeight(paint) - bottom + centerY), paint);
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
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 Windowz 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 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/wj/sticky/demo/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.wj.sticky.demo;
2 |
3 | import android.graphics.Color;
4 | import android.os.Bundle;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.support.v7.widget.Toolbar;
7 | import android.view.View;
8 | import android.widget.RelativeLayout;
9 | import android.widget.Toast;
10 |
11 | import com.wj.sticky.StickyFlagView;
12 | import com.wj.utils.ScreenUtils;
13 |
14 | public class MainActivity extends AppCompatActivity {
15 |
16 | @Override
17 | protected void onCreate(Bundle savedInstanceState) {
18 | super.onCreate(savedInstanceState);
19 | setContentView(R.layout.activity_main);
20 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
21 | setSupportActionBar(toolbar);
22 |
23 | final StickyFlagView stickyFlagView1 = (StickyFlagView) findViewById(R.id.sticky_view1);
24 | final StickyFlagView stickyFlagView2 = (StickyFlagView) findViewById(R.id.sticky_view2);
25 | stickyFlagView1.setOnFlagDisappearListener(new StickyFlagView.OnFlagDisappearListener() {
26 | @Override
27 | public void onFlagDisappear(StickyFlagView view) {
28 | Toast.makeText(MainActivity.this, "Flag have disappeared.", Toast.LENGTH_SHORT).show();
29 | }
30 | });
31 | stickyFlagView1.setFlagText("6");
32 |
33 | findViewById(R.id.btn_flag).setOnClickListener(new View.OnClickListener() {
34 | @Override
35 | public void onClick(View v) {
36 | stickyFlagView1.setFlagText("6");
37 | stickyFlagView2.setFlagText(null);
38 | }
39 | });
40 |
41 | final RelativeLayout rl = (RelativeLayout) findViewById(R.id.rl);
42 | findViewById(R.id.btn_add).setOnClickListener(new View.OnClickListener() {
43 | @Override
44 | public void onClick(View v) {
45 | StickyFlagView sfv = new StickyFlagView(MainActivity.this);
46 | sfv.setFlagText("9");
47 | sfv.setFlagTextColor(Color.WHITE);
48 | sfv.setFlagTextSize(ScreenUtils.spTopx(MainActivity.this, 15));
49 | sfv.setFlagColor(Color.BLUE);
50 | sfv.setFlagRadius(ScreenUtils.dpToPx(MainActivity.this, 10));
51 | sfv.setMinStickRadius(ScreenUtils.dpToPx(MainActivity.this, 3));
52 | sfv.setMaxStickRadius(ScreenUtils.dpToPx(MainActivity.this, 8));
53 | sfv.setMaxDistance(ScreenUtils.dpToPx(MainActivity.this, 80));
54 | // sfv.setFlagDrawable(R.drawable.bubble);
55 |
56 | RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
57 | (int) ScreenUtils.dpToPx(MainActivity.this, 40),
58 | (int) ScreenUtils.dpToPx(MainActivity.this, 40));
59 | lp.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
60 | lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
61 |
62 | rl.addView(sfv, lp);
63 | }
64 | });
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
26 |
27 |
34 |
35 |
41 |
42 |
43 |
47 |
48 |
52 |
53 |
59 |
60 |
68 |
69 |
70 |
74 |
75 |
80 |
81 |
86 |
87 |
--------------------------------------------------------------------------------
/StickyFlagView/src/main/java/com/wj/utils/ScreenUtils.java:
--------------------------------------------------------------------------------
1 | package com.wj.utils;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.graphics.Bitmap;
6 | import android.graphics.Rect;
7 | import android.util.DisplayMetrics;
8 | import android.view.View;
9 | import android.view.WindowManager;
10 |
11 | /**
12 | * 获得屏幕相关的辅助类
13 | */
14 | public class ScreenUtils
15 | {
16 | private ScreenUtils()
17 | {
18 | throw new UnsupportedOperationException("cannot be instantiated");
19 | }
20 |
21 | /**
22 | * 获得屏幕宽度
23 | */
24 | public static int getScreenWidth(Context context)
25 | {
26 | WindowManager wm = (WindowManager) context
27 | .getSystemService(Context.WINDOW_SERVICE);
28 | DisplayMetrics outMetrics = new DisplayMetrics();
29 | wm.getDefaultDisplay().getMetrics(outMetrics);
30 | return outMetrics.widthPixels;
31 | }
32 |
33 | /**
34 | * 获得屏幕高度
35 | */
36 | public static int getScreenHeight(Context context)
37 | {
38 | WindowManager wm = (WindowManager) context
39 | .getSystemService(Context.WINDOW_SERVICE);
40 | DisplayMetrics outMetrics = new DisplayMetrics();
41 | wm.getDefaultDisplay().getMetrics(outMetrics);
42 | return outMetrics.heightPixels;
43 | }
44 |
45 | /**
46 | * 获得状态栏高度
47 | */
48 | public static int getStatusHeight(Context context)
49 | {
50 |
51 | int statusHeight = -1;
52 | try
53 | {
54 | Class> clazz = Class.forName("com.android.internal.R$dimen");
55 | Object object = clazz.newInstance();
56 | int height = Integer.parseInt(clazz.getField("status_bar_height")
57 | .get(object).toString());
58 | statusHeight = context.getResources().getDimensionPixelSize(height);
59 | } catch (Exception e)
60 | {
61 | e.printStackTrace();
62 | }
63 | return statusHeight;
64 | }
65 |
66 | /**
67 | * 获取当前屏幕截图,包含状态栏
68 | */
69 | public static Bitmap snapShotWithStatusBar(Activity activity)
70 | {
71 | View view = activity.getWindow().getDecorView();
72 | view.setDrawingCacheEnabled(true);
73 | view.buildDrawingCache();
74 | Bitmap bmp = view.getDrawingCache();
75 | int width = getScreenWidth(activity);
76 | int height = getScreenHeight(activity);
77 | Bitmap bp = null;
78 | bp = Bitmap.createBitmap(bmp, 0, 0, width, height);
79 | view.destroyDrawingCache();
80 | return bp;
81 |
82 | }
83 |
84 | /**
85 | * 获取当前屏幕截图,不包含状状态栏
86 | */
87 | public static Bitmap snapShotWithoutStatusBar(Activity activity)
88 | {
89 | View view = activity.getWindow().getDecorView();
90 | view.setDrawingCacheEnabled(true);
91 | view.buildDrawingCache();
92 | Bitmap bmp = view.getDrawingCache();
93 | Rect frame = new Rect();
94 | activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
95 | int statusBarHeight = frame.top;
96 |
97 | int width = getScreenWidth(activity);
98 | int height = getScreenHeight(activity);
99 | Bitmap bp = null;
100 | bp = Bitmap.createBitmap(bmp, 0, statusBarHeight, width, height
101 | - statusBarHeight);
102 | view.destroyDrawingCache();
103 | return bp;
104 | }
105 |
106 | public static float dpToPx(Context context, float dp) {
107 | if (context == null) {
108 | return -1;
109 | }
110 | return dp * context.getResources().getDisplayMetrics().density;
111 | }
112 |
113 | public static float spTopx(Context context, float sp) {
114 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
115 | return (int) (sp * fontScale + 0.5f);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/sample/sample.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | generateDebugSources
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 |
--------------------------------------------------------------------------------
/StickyFlagView/StickyFlagView-StickyFlagView.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | generateDebugSources
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 |
--------------------------------------------------------------------------------
/StickyFlagView/src/main/java/com/wj/sticky/StickyFlagView.java:
--------------------------------------------------------------------------------
1 | package com.wj.sticky;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Bitmap;
6 | import android.graphics.Canvas;
7 | import android.graphics.Color;
8 | import android.graphics.Paint;
9 | import android.graphics.Path;
10 | import android.graphics.PixelFormat;
11 | import android.graphics.PointF;
12 | import android.graphics.drawable.BitmapDrawable;
13 | import android.graphics.drawable.Drawable;
14 | import android.support.annotation.ColorInt;
15 | import android.support.annotation.DrawableRes;
16 | import android.text.TextUtils;
17 | import android.util.AttributeSet;
18 | import android.view.Gravity;
19 | import android.view.MotionEvent;
20 | import android.view.View;
21 | import android.view.ViewGroup;
22 | import android.view.WindowManager;
23 | import android.view.animation.BounceInterpolator;
24 | import android.widget.FrameLayout;
25 | import android.widget.RelativeLayout;
26 |
27 | import com.nineoldandroids.animation.Animator;
28 | import com.nineoldandroids.animation.AnimatorListenerAdapter;
29 | import com.nineoldandroids.animation.ValueAnimator;
30 | import com.wj.utils.DrawUtils;
31 | import com.wj.utils.ScreenUtils;
32 |
33 | import java.util.ArrayList;
34 | import java.util.List;
35 |
36 | /**
37 | * 有粘性的标记view
38 | * Created by jia.wei on 16/1/22.
39 | */
40 | public class StickyFlagView extends View {
41 |
42 | private Context context;
43 |
44 | private ViewGroup parent;
45 | private ViewGroup.LayoutParams originalLp; // view原始layout
46 | private int[] originalLocation; // view原始的location
47 | private int originalWidth; // view原始的宽度
48 | private int originalHeight; // view原始的高度
49 |
50 | private float stickRadius; // 黏贴半径
51 |
52 | private int flagColor; // 标记颜色
53 | private int flagTextColor; // 标记文本颜色
54 | private float maxDragDistance; // 最大拖拽距离
55 | private String flagText; // 标记文本
56 | private float flagTextSize; // 标记文本大小
57 | private float flagRadius; // 标记半径
58 | private Bitmap flagBitmap; // 标记图片
59 | private float maxStickRadius; // 最大黏贴半径
60 | private float minStickRadius; // 最小黏贴半径
61 | private float rate = 0.8f;
62 |
63 | private boolean isFirstSizeChange = true;
64 | private boolean isTouched;
65 | private boolean isReachLimit; // 是否达到最大拖拽距离
66 | private boolean isRollBackAnimating; // 回滚动画是否在执行
67 | private boolean isDisappearAnimating; // 消失动画是否在执行
68 | private boolean isFlagDisappear; // 标记是否消失
69 | private boolean isViewLoadFinish; // view是否加载完毕
70 | private boolean isViewInWindow; // view是否在window中
71 |
72 | private int which;
73 | private List disappearRes;
74 |
75 | private PointF stickPoint; // 黏贴点
76 | private PointF dragFlagPoint; // 拖拽标记点
77 | private Paint flagPaint; // 标记画笔
78 | private Paint flagTextPaint; // 标记文本画笔
79 | private Path flagPath;
80 |
81 | private OnFlagDisappearListener listener;
82 | private WindowManager windowManager;
83 |
84 | public StickyFlagView(Context context) {
85 | super(context);
86 | this.context = context;
87 | init();
88 | }
89 |
90 | public StickyFlagView(Context context, AttributeSet attrs) {
91 | super(context, attrs);
92 | this.context = context;
93 |
94 | initViewProperty(context, attrs);
95 | init();
96 | }
97 |
98 | public StickyFlagView(Context context, AttributeSet attrs, int defStyleAttr) {
99 | super(context, attrs, defStyleAttr);
100 | this.context = context;
101 |
102 | initViewProperty(context, attrs);
103 | init();
104 | }
105 |
106 | /**
107 | * 初始化view属性
108 | */
109 | private void initViewProperty(Context context, AttributeSet attrs) {
110 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StickyFlagView);
111 |
112 | flagColor = typedArray.getColor(R.styleable.StickyFlagView_flagColor, Color.RED);
113 | flagTextColor = typedArray.getColor(R.styleable.StickyFlagView_flagTextColor, Color.WHITE);
114 | flagTextSize = typedArray.getDimension(R.styleable.StickyFlagView_flagTextSize, ScreenUtils.spTopx(context, 12));
115 | maxDragDistance = typedArray.getDimension(R.styleable.StickyFlagView_maxDistance, ScreenUtils.getScreenHeight(context) / 6);
116 | minStickRadius = typedArray.getDimension(R.styleable.StickyFlagView_minStickRadius, ScreenUtils.dpToPx(context, 2));
117 | flagRadius = typedArray.getDimension(R.styleable.StickyFlagView_flagRadius, ScreenUtils.dpToPx(context, 10));
118 | maxStickRadius = typedArray.getDimension(R.styleable.StickyFlagView_maxStickRadius, flagRadius * rate);
119 |
120 | Drawable flagDrawable = typedArray.getDrawable(R.styleable.StickyFlagView_flagDrawable);
121 | if (flagDrawable != null) {
122 | flagBitmap = ((BitmapDrawable) flagDrawable).getBitmap();
123 | }
124 |
125 | typedArray.recycle();
126 | }
127 |
128 | private void init() {
129 | // 处理onDraw方法不执行的问题
130 | setWillNotDraw(false);
131 |
132 | // 这些默认值是为第一个构造函数准备的
133 | if (flagColor == 0) {
134 | flagColor = Color.RED;
135 | }
136 | if (flagTextColor == 0) {
137 | flagTextColor = Color.WHITE;
138 | }
139 | if (flagTextSize == 0) {
140 | flagTextSize = ScreenUtils.spTopx(context, 12);
141 | }
142 | if (flagRadius == 0) {
143 | flagRadius = ScreenUtils.dpToPx(context, 10);
144 | }
145 | if (maxDragDistance == 0) {
146 | maxDragDistance = ScreenUtils.getScreenHeight(context) / 6;
147 | }
148 | if (minStickRadius == 0) {
149 | minStickRadius = ScreenUtils.dpToPx(context, 2);
150 | }
151 | if (maxStickRadius == 0) {
152 | maxStickRadius = flagRadius * rate;
153 | }
154 |
155 | originalLocation = new int[2];
156 | stickPoint = new PointF();
157 | dragFlagPoint = new PointF();
158 | flagPath = new Path();
159 |
160 | flagPaint = new Paint();
161 | flagPaint.setAntiAlias(true);
162 | flagPaint.setColor(flagColor);
163 |
164 | flagTextPaint = new Paint();
165 | flagTextPaint.setAntiAlias(true);
166 | flagTextPaint.setColor(flagTextColor);
167 | flagTextPaint.setTextSize(flagTextSize);
168 |
169 | windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
170 | }
171 |
172 | @Override
173 | public void setLayoutParams(ViewGroup.LayoutParams params) {
174 | super.setLayoutParams(params);
175 |
176 | isViewLoadFinish = false;
177 | this.post(new Runnable() {
178 | @Override
179 | public void run() {
180 | isViewLoadFinish = true;
181 | }
182 | });
183 | }
184 |
185 | @Override
186 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
187 | int width = 0;
188 | int widthMode = MeasureSpec.getMode(widthMeasureSpec);
189 | if (widthMode == MeasureSpec.UNSPECIFIED) {
190 | if (flagBitmap == null) {
191 | width = (int) ScreenUtils.dpToPx(context, 20);
192 | } else {
193 | width = flagBitmap.getWidth();
194 | }
195 | } else if (widthMode == MeasureSpec.EXACTLY){
196 | width = MeasureSpec.getSize(widthMeasureSpec);
197 | } else if (widthMode == MeasureSpec.AT_MOST) {
198 | if (flagBitmap == null) {
199 | width = (int) ScreenUtils.dpToPx(context, 20);
200 | } else {
201 | width = flagBitmap.getWidth();
202 | }
203 | }
204 |
205 | int height = 0;
206 | int heightMode = MeasureSpec.getMode(heightMeasureSpec);
207 | if (heightMode == MeasureSpec.UNSPECIFIED) {
208 | if (flagBitmap == null) {
209 | height = (int) ScreenUtils.dpToPx(context, 20);
210 | } else {
211 | height = flagBitmap.getHeight();
212 | }
213 | } else if (heightMode == MeasureSpec.EXACTLY){
214 | height = MeasureSpec.getSize(heightMeasureSpec);
215 | } else if (heightMode == MeasureSpec.AT_MOST) {
216 | if (flagBitmap == null) {
217 | height = (int) ScreenUtils.dpToPx(context, 20);
218 | } else {
219 | height = flagBitmap.getHeight();
220 | }
221 | }
222 |
223 | setMeasuredDimension(width, height);
224 | }
225 |
226 | @Override
227 | protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
228 | super.onSizeChanged(width, height, oldWidth, oldHeight);
229 | if (isFirstSizeChange) {
230 | parent = (ViewGroup) getParent();
231 | // StickyFlagView的父控件只能是RelativeLayout或FrameLayout
232 | if (!(parent instanceof RelativeLayout || parent instanceof FrameLayout)) {
233 | throw new RuntimeException("StickyFlagView can only be placed on the RelativeLayout or FrameLayout.");
234 | }
235 |
236 | // 记录view原始layout参数
237 | originalLp = getLayoutParams();
238 | originalWidth = width;
239 | originalHeight = height;
240 |
241 | getLocationOnScreen(originalLocation);
242 | originalLocation[1] = originalLocation[1] - ScreenUtils.getStatusHeight(context);
243 |
244 | if (flagBitmap == null) {
245 | float radius = Math.min(originalWidth, originalHeight) * 0.5f;
246 | flagRadius = flagRadius > radius ? radius : flagRadius;
247 | stickRadius = flagRadius > maxStickRadius ? maxStickRadius : flagRadius * rate;
248 | } else {
249 | // 黏贴半径不能超过图片宽和高的最小值的一半
250 | flagRadius = Math.min(flagBitmap.getWidth(), flagBitmap.getHeight()) * 0.5f;
251 | stickRadius = maxStickRadius > flagRadius ? flagRadius * rate : maxStickRadius;
252 | }
253 |
254 | // 黏贴点在原始view的中心点
255 | stickPoint.set((float) (originalWidth * 0.5), (float) (originalHeight * 0.5));
256 | isFirstSizeChange = false;
257 | } else {
258 | // view的size改变之后,修正黏贴点坐标
259 | if (originalWidth == width && originalHeight == height) {
260 | stickPoint.set((float) (originalWidth * 0.5), (float) (originalHeight * 0.5));
261 | } else {
262 | stickPoint.x += originalLocation[0];
263 | stickPoint.y += originalLocation[1];
264 | }
265 | }
266 | dragFlagPoint.x = stickPoint.x;
267 | dragFlagPoint.y = stickPoint.y;
268 | }
269 |
270 | @Override
271 | protected void onDraw(Canvas canvas) {
272 | super.onDraw(canvas);
273 | canvas.drawColor(Color.TRANSPARENT);
274 |
275 | if (!isFlagDisappear) {
276 | if (isTouched || isRollBackAnimating) {
277 | if (!isReachLimit) {
278 | drawStickCircle(canvas);
279 | drawStickCurve(canvas);
280 | }
281 | drawDragFlag(canvas);
282 | } else {
283 | if (!isDisappearAnimating) {
284 | drawDragFlag(canvas);
285 | }
286 | }
287 |
288 | if (isDisappearAnimating) {
289 | drawDisappearFlagBitmap(canvas);
290 | }
291 |
292 | if (!isReachLimit) {
293 | drawFlagText(canvas);
294 | } else {
295 | if (!isDisappearAnimating) {
296 | drawFlagText(canvas);
297 | }
298 | }
299 | }
300 | }
301 |
302 | /**
303 | * 绘制黏贴圆点
304 | */
305 | private void drawStickCircle(Canvas canvas) {
306 | canvas.drawCircle(stickPoint.x, stickPoint.y, stickRadius, flagPaint);
307 | }
308 |
309 | /**
310 | * 绘制拖拽标记
311 | */
312 | private void drawDragFlag(Canvas canvas) {
313 | if (flagBitmap == null) {
314 | canvas.drawCircle(dragFlagPoint.x, dragFlagPoint.y, flagRadius, flagPaint);
315 | } else {
316 | canvas.drawBitmap(flagBitmap, dragFlagPoint.x - flagBitmap.getWidth() * 0.5f,
317 | dragFlagPoint.y - flagBitmap.getHeight() * 0.5f, flagPaint);
318 | }
319 | }
320 |
321 | /**
322 | * 绘制黏贴曲线
323 | */
324 | private void drawStickCurve(Canvas canvas) {
325 | float stickOffsetX = (float) (stickRadius * Math.sin(Math.atan((dragFlagPoint.y - stickPoint.y) / (dragFlagPoint.x - stickPoint.x))));
326 | float stickOffsetY = (float) (stickRadius * Math.cos(Math.atan((dragFlagPoint.y - stickPoint.y) / (dragFlagPoint.x - stickPoint.x))));
327 | float flagOffsetX = (float) (flagRadius * Math.sin(Math.atan((dragFlagPoint.y - stickPoint.y) / (dragFlagPoint.x - stickPoint.x))));
328 | float flagOffsetY = (float) (flagRadius * Math.cos(Math.atan((dragFlagPoint.y - stickPoint.y) / (dragFlagPoint.x - stickPoint.x))));
329 |
330 | float x1 = stickPoint.x - stickOffsetX;
331 | float y1 = stickPoint.y + stickOffsetY;
332 |
333 | float x2 = dragFlagPoint.x - flagOffsetX;
334 | float y2 = dragFlagPoint.y + flagOffsetY;
335 |
336 | float x3 = dragFlagPoint.x + flagOffsetX;
337 | float y3 = dragFlagPoint.y - flagOffsetY;
338 |
339 | float x4 = stickPoint.x + stickOffsetX;
340 | float y4 = stickPoint.y - stickOffsetY;
341 |
342 | // 曲线控制点
343 | float controlPointX = (float) ((stickPoint.x + dragFlagPoint.x) * 0.5);
344 | float controlPointY = (float) ((stickPoint.y + dragFlagPoint.y) * 0.5);
345 |
346 | flagPath.reset();
347 | flagPath.moveTo(x1, y1);
348 | flagPath.quadTo(controlPointX, controlPointY, x2, y2);
349 | flagPath.lineTo(x3, y3);
350 | flagPath.quadTo(controlPointX, controlPointY, x4, y4);
351 | flagPath.lineTo(x1, y1);
352 |
353 | canvas.drawPath(flagPath, flagPaint);
354 | }
355 |
356 | /**
357 | * 绘制标记文本
358 | */
359 | private void drawFlagText(Canvas canvas) {
360 | if (!TextUtils.isEmpty(flagText)) {
361 | DrawUtils.drawTextInCenter(canvas, flagTextPaint, flagText, dragFlagPoint.x, dragFlagPoint.y);
362 | }
363 | }
364 |
365 | /**
366 | * 绘制标记消失的Bitmap
367 | */
368 | private void drawDisappearFlagBitmap(Canvas canvas) {
369 | if (disappearRes != null) {
370 | Drawable drawable = context.getResources().getDrawable(disappearRes.get(which));
371 | if (drawable != null) {
372 | Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
373 | canvas.drawBitmap(bitmap, (float) (dragFlagPoint.x - bitmap.getWidth() * 0.5),
374 | (float) (dragFlagPoint.y - bitmap.getHeight() * 0.5), flagPaint);
375 | }
376 | }
377 | }
378 |
379 | @Override
380 | public boolean onTouchEvent(MotionEvent event) {
381 | if (!isViewLoadFinish || isRollBackAnimating || isDisappearAnimating || isFlagDisappear) {
382 | return true;
383 | }
384 | switch (event.getAction()) {
385 | case MotionEvent.ACTION_DOWN:
386 | isTouched = true;
387 | addViewInWindow();
388 | break;
389 | case MotionEvent.ACTION_MOVE:
390 | dragFlagPoint.x = event.getRawX();
391 | dragFlagPoint.y = event.getRawY() - ScreenUtils.getStatusHeight(context);
392 |
393 | double distance = Math.sqrt(Math.pow(dragFlagPoint.y - stickPoint.y, 2) + Math.pow(dragFlagPoint.x - stickPoint.x, 2));
394 | if (distance > maxDragDistance) {
395 | isReachLimit = true;
396 | } else {
397 | isReachLimit = false;
398 | stickRadius = (float) (maxStickRadius * (1 - distance / maxDragDistance));
399 | stickRadius = stickRadius < minStickRadius ? minStickRadius : stickRadius;
400 | }
401 |
402 | postInvalidate();
403 | break;
404 | case MotionEvent.ACTION_UP:
405 | isTouched = false;
406 | if (isReachLimit) {
407 | launchDisappearAnimation(1000);
408 | if (listener != null) {
409 | listener.onFlagDisappear(this);
410 | }
411 | } else {
412 | launchRollBackAnimation(300);
413 | }
414 | break;
415 | }
416 | return true;
417 | }
418 |
419 | /**
420 | * 把view加入应用窗口
421 | */
422 | private void addViewInWindow() {
423 | if (isViewLoadFinish && !isViewInWindow) {
424 | if (parent != null) {
425 | // 将view从它的父控件中移除
426 | parent.removeView(this);
427 | }
428 |
429 | WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
430 | layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
431 | layoutParams.format = PixelFormat.TRANSPARENT;
432 | layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
433 | layoutParams.gravity = Gravity.START | Gravity.TOP;
434 | layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
435 | layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;
436 | layoutParams.x = 0;
437 | layoutParams.y = 0;
438 |
439 | if (windowManager != null) {
440 | // 将view加入window
441 | windowManager.addView(this, layoutParams);
442 | post(new Runnable() {
443 | @Override
444 | public void run() {
445 | isViewInWindow = true;
446 | }
447 | });
448 | }
449 | }
450 | }
451 |
452 | /**
453 | * 还原view
454 | */
455 | private void restoreView() {
456 | if (isViewLoadFinish) {
457 | // 还原黏贴半径
458 | stickRadius = flagRadius > maxStickRadius ? maxStickRadius : flagRadius * rate;
459 | isReachLimit = false;
460 |
461 | if (windowManager != null && isViewInWindow) {
462 | // 把view从window中移除
463 | windowManager.removeView(this);
464 | isViewInWindow = false;
465 |
466 | if (parent != null) {
467 | parent.addView(this, originalLp);
468 | // 在高版本的SDK上,没有这段代码,view可能不会刷新
469 | post(new Runnable() {
470 | @Override
471 | public void run() {
472 | parent.invalidate();
473 | }
474 | });
475 | }
476 | }
477 | } else {
478 | post(new Runnable() {
479 | @Override
480 | public void run() {
481 | restoreView();
482 | }
483 | });
484 | }
485 | }
486 |
487 | /**
488 | * 启动标记的消失动画
489 | */
490 | private void launchDisappearAnimation(long duration) {
491 | disappearRes = new ArrayList<>();
492 | disappearRes.add(R.drawable.disappear0);
493 | disappearRes.add(R.drawable.disappear1);
494 | disappearRes.add(R.drawable.disappear2);
495 | disappearRes.add(R.drawable.disappear3);
496 | disappearRes.add(R.drawable.disappear4);
497 |
498 | isDisappearAnimating = true;
499 | ValueAnimator animator = ValueAnimator.ofInt(0, 4);
500 | animator.setDuration(duration);
501 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
502 | @Override
503 | public void onAnimationUpdate(ValueAnimator animation) {
504 | which = (int) animation.getAnimatedValue();
505 | postInvalidate();
506 | }
507 | });
508 | animator.addListener(new AnimatorListenerAdapter() {
509 | @Override
510 | public void onAnimationEnd(Animator animation) {
511 | super.onAnimationEnd(animation);
512 | isFlagDisappear = true;
513 | isDisappearAnimating = false;
514 | restoreView();
515 | }
516 | });
517 | animator.start();
518 | }
519 |
520 | /**
521 | * 启动标记的回滚动画
522 | */
523 | private void launchRollBackAnimation(long duration) {
524 | isRollBackAnimating = true;
525 | // 过黏贴点和拖拽点的直线的斜率
526 | final float slope = (dragFlagPoint.y - stickPoint.y) / (dragFlagPoint.x - stickPoint.x);
527 |
528 | // 有拖拽点向黏贴点靠近
529 | ValueAnimator rollBackAnim = ValueAnimator.ofFloat(dragFlagPoint.x, stickPoint.x);
530 | rollBackAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
531 | @Override
532 | public void onAnimationUpdate(ValueAnimator animation) {
533 | float curX = (float) animation.getAnimatedValue();
534 | // 由过黏贴点和拖拽点的直线的方程求出y坐标
535 | float curY = slope * (curX - stickPoint.x) + stickPoint.y;
536 |
537 | dragFlagPoint.x = curX;
538 | dragFlagPoint.y = curY;
539 | double distance = Math.sqrt(Math.pow(dragFlagPoint.y - stickPoint.y, 2) + Math.pow(dragFlagPoint.x - stickPoint.x, 2));
540 | stickRadius = (float) (maxStickRadius * (1 - distance / maxDragDistance));
541 |
542 | postInvalidate();
543 | }
544 | });
545 | rollBackAnim.addListener(new AnimatorListenerAdapter() {
546 | @Override
547 | public void onAnimationEnd(Animator animation) {
548 | super.onAnimationEnd(animation);
549 | isRollBackAnimating = false;
550 | restoreView();
551 | }
552 | });
553 | rollBackAnim.setInterpolator(new BounceInterpolator()); // 弹跳插值器
554 | rollBackAnim.setDuration(duration);
555 | rollBackAnim.start();
556 | }
557 |
558 | public String getFlagText() {
559 | return flagText;
560 | }
561 |
562 | public void setFlagText(String flagText) {
563 | this.flagText = flagText;
564 | isFlagDisappear = false;
565 |
566 | requestParentInvalidate();
567 | }
568 |
569 | public void setFlagColor(@ColorInt int flagColor) {
570 | if (flagPaint != null) {
571 | flagPaint.setColor(flagColor);
572 | }
573 | }
574 |
575 | public void setFlagTextColor(@ColorInt int flagTextColor) {
576 | if (flagTextPaint != null) {
577 | flagTextPaint.setColor(flagTextColor);
578 | }
579 | }
580 |
581 | public void setMaxDistance(float maxDistance) {
582 | this.maxDragDistance = maxDistance;
583 | }
584 |
585 | public void setFlagTextSize(float flagTextSize) {
586 | if (flagTextPaint != null) {
587 | flagTextPaint.setTextSize(flagTextSize);
588 | }
589 | }
590 |
591 | public void setFlagRadius(float flagRadius) {
592 | this.flagRadius = flagRadius;
593 | }
594 |
595 | public void setFlagDrawable(@DrawableRes int drawableRes) {
596 | Drawable drawable = context.getResources().getDrawable(drawableRes);
597 | if (drawable != null) {
598 | this.flagBitmap = ((BitmapDrawable) drawable).getBitmap();
599 | }
600 | }
601 |
602 | public void setMaxStickRadius(float maxStickRadius) {
603 | this.maxStickRadius = maxStickRadius;
604 | }
605 |
606 | public void setMinStickRadius(float minStickRadius) {
607 | this.minStickRadius = minStickRadius;
608 | }
609 |
610 | private void requestParentInvalidate() {
611 | if (parent != null) {
612 | // 此处尝试过使用this.invalidate(), 但不起作用, view不会刷新
613 | // 可能与布局的优化机制有关,它认为并不需要重绘
614 | parent.invalidate();
615 | }
616 | }
617 |
618 | public void setOnFlagDisappearListener(OnFlagDisappearListener listener) {
619 | this.listener = listener;
620 | }
621 |
622 | public interface OnFlagDisappearListener {
623 | void onFlagDisappear(StickyFlagView view);
624 | }
625 |
626 | }
627 |
--------------------------------------------------------------------------------