├── .idea
├── .name
├── copyright
│ └── profiles_settings.xml
├── encodings.xml
├── vcs.xml
├── runConfigurations.xml
├── compiler.xml
├── modules.xml
├── gradle.xml
└── misc.xml
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── copy.png
│ │ │ │ ├── heart.png
│ │ │ │ ├── info.png
│ │ │ │ ├── like.png
│ │ │ │ ├── mark.png
│ │ │ │ ├── record.png
│ │ │ │ ├── refresh.png
│ │ │ │ ├── search.png
│ │ │ │ ├── settings.png
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── 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
│ │ │ └── rance
│ │ │ └── sectormenu
│ │ │ └── MainActivity.java
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── rance
│ │ │ └── sectormenu
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── rance
│ │ └── sectormenu
│ │ └── ApplicationTest.java
├── proguard-rules.pro
└── build.gradle
├── library
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ └── values
│ │ │ ├── strings.xml
│ │ │ └── attr.xml
│ │ ├── java
│ │ └── com
│ │ │ └── rance
│ │ │ └── library
│ │ │ ├── ButtonEventListener.java
│ │ │ ├── QuickClickChecker.java
│ │ │ ├── Blur.java
│ │ │ ├── AngleCalculator.java
│ │ │ ├── ButtonData.java
│ │ │ └── SectorMenuButton.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | SectorMenu
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':library'
2 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SectorMenu
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Library
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xhdpi/copy.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/heart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xhdpi/heart.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xhdpi/info.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/like.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xhdpi/like.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/mark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xhdpi/mark.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/record.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xhdpi/record.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xhdpi/refresh.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xhdpi/search.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xhdpi/settings.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rance935/SectorMenu/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/src/main/java/com/rance/library/ButtonEventListener.java:
--------------------------------------------------------------------------------
1 | package com.rance.library;
2 |
3 | /**
4 | * 作者:Rance on 2016/11/10 16:41
5 | * 邮箱:rance935@163.com
6 | */
7 | public interface ButtonEventListener {
8 | void onButtonClicked(int index);
9 | void onExpand();
10 | void onCollapse();
11 | }
12 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/test/java/com/rance/sectormenu/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.rance.sectormenu;
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 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/rance/sectormenu/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.rance.sectormenu;
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 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/rance/library/QuickClickChecker.java:
--------------------------------------------------------------------------------
1 | package com.rance.library;
2 |
3 | /**
4 | * 作者:Rance on 2016/11/10 16:41
5 | * 邮箱:rance935@163.com
6 | */
7 | public class QuickClickChecker {
8 | private int threshold;
9 | private long lastClickTime = 0;
10 |
11 | public QuickClickChecker(int threshold) {
12 | this.threshold = threshold;
13 | }
14 |
15 | public boolean isQuick() {
16 | boolean isQuick = System.currentTimeMillis() - lastClickTime <= threshold;
17 | lastClickTime = System.currentTimeMillis();
18 | return isQuick;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/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 C:\Users\Administrator\AppData\Local\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 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/library/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 C:\Users\Administrator\AppData\Local\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 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 24
5 | buildToolsVersion "24.0.2"
6 |
7 | defaultConfig {
8 | minSdkVersion 15
9 | targetSdkVersion 24
10 | versionCode 1
11 | versionName "1.0"
12 |
13 | //RenderScript向下兼容
14 | renderscriptTargetApi 18
15 | renderscriptSupportModeEnabled true
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | compile fileTree(dir: 'libs', include: ['*.jar'])
27 | testCompile 'junit:junit:4.12'
28 | compile 'com.android.support:appcompat-v7:24.2.0'
29 | }
30 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 24
5 | buildToolsVersion "24.0.2"
6 |
7 | defaultConfig {
8 | applicationId "com.rance.sectormenu"
9 | minSdkVersion 15
10 | targetSdkVersion 24
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | //RenderScript向下兼容
15 | renderscriptTargetApi 18
16 | renderscriptSupportModeEnabled true
17 | }
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | }
25 |
26 | dependencies {
27 | compile fileTree(dir: 'libs', include: ['*.jar'])
28 | testCompile 'junit:junit:4.12'
29 | compile 'com.android.support:appcompat-v7:24.2.0'
30 | compile project(path: ':library')
31 | }
32 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/library/src/main/res/values/attr.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 |
--------------------------------------------------------------------------------
/.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 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/src/main/java/com/rance/library/Blur.java:
--------------------------------------------------------------------------------
1 | package com.rance.library;
2 |
3 | import android.annotation.TargetApi;
4 | import android.content.Context;
5 | import android.graphics.Bitmap;
6 | import android.os.Build;
7 | import android.os.Handler;
8 | import android.os.Looper;
9 | import android.support.annotation.WorkerThread;
10 | import android.support.v8.renderscript.Allocation;
11 | import android.support.v8.renderscript.Element;
12 | import android.support.v8.renderscript.RenderScript;
13 | import android.support.v8.renderscript.ScriptIntrinsicBlur;
14 |
15 | /**
16 | * 作者:Rance on 2016/11/10 16:41
17 | * 邮箱:rance935@163.com
18 | */
19 | public class Blur {
20 | private static final float SCALE = 0.4F;
21 |
22 | private float radius;
23 |
24 | private Thread blurThread;
25 | private Context context;
26 | private Bitmap inBitmap;
27 | private Callback callback;
28 |
29 | public Blur() {
30 | initThread();
31 | }
32 |
33 | private void initThread() {
34 | blurThread = new Thread(new Runnable() {
35 | @Override
36 | public void run() {
37 | final Bitmap blurred = getBlurBitmap(context, inBitmap, radius);
38 | Handler handler = new Handler(Looper.getMainLooper());
39 | handler.post(new Runnable() {
40 | @Override
41 | public void run() {
42 | if (callback != null) {
43 | callback.onBlurred(blurred);
44 | }
45 | }
46 | });
47 | }
48 | });
49 | }
50 |
51 | public void setParams(Callback callback, Context context, Bitmap inBitmap, float radius) {
52 | this.callback = callback;
53 | this.context = context;
54 | this.inBitmap = inBitmap;
55 | this.radius = radius;
56 | }
57 |
58 | public void execute() {
59 | blurThread.run();
60 | }
61 |
62 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
63 | @WorkerThread
64 | private Bitmap getBlurBitmap(Context context, Bitmap inBitmap, float radius) {
65 | if (context == null || inBitmap == null) {
66 | throw new IllegalArgumentException("have not called setParams() before call execute()");
67 | }
68 |
69 | int width = Math.round(inBitmap.getWidth() * SCALE);
70 | int height = Math.round(inBitmap.getHeight() * SCALE);
71 |
72 | Bitmap in = Bitmap.createScaledBitmap(inBitmap, width, height, false);
73 | Bitmap out = Bitmap.createBitmap(in);
74 |
75 | RenderScript rs = RenderScript.create(context);
76 | ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
77 |
78 | Allocation allocationIn = Allocation.createFromBitmap(rs, in);
79 | Allocation allocationOut = Allocation.createFromBitmap(rs, out);
80 |
81 | blurScript.setRadius(radius);
82 | blurScript.setInput(allocationIn);
83 | blurScript.forEach(allocationOut);
84 | allocationOut.copyTo(out);
85 |
86 | allocationIn.destroy();
87 | allocationOut.destroy();
88 | blurScript.destroy();
89 | rs.destroy();
90 |
91 | return out;
92 | }
93 |
94 | public interface Callback {
95 | void onBlurred(Bitmap blurredBitmap);
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/library/src/main/java/com/rance/library/AngleCalculator.java:
--------------------------------------------------------------------------------
1 | package com.rance.library;
2 |
3 | /**
4 | * 作者:Rance on 2016/11/10 16:41
5 | * 邮箱:rance935@163.com
6 | */
7 | public class AngleCalculator {
8 |
9 | private double startAngleRadians;
10 | private double averageAngleRadians;
11 | private boolean angleStartEqualsEnd;
12 |
13 | /**
14 | * @param startAngleDegree the value of the attribute aebStartAngleDegree
15 | * @param endAngleDegree the value of the attribute aebEndAngleDegree
16 | * @param expandButtonCount the count of buttons that will expand
17 | */
18 | public AngleCalculator(float startAngleDegree, float endAngleDegree, int expandButtonCount) {
19 | angleStartEqualsEnd = (endAngleDegree - startAngleDegree) == 0;
20 | startAngleDegree = startAngleDegree % 360;
21 | endAngleDegree = endAngleDegree % 360;
22 | this.startAngleRadians = Math.toRadians(startAngleDegree);
23 | double endAngleRadians = Math.toRadians(endAngleDegree);
24 | if (expandButtonCount > 1) {
25 | this.averageAngleRadians = (endAngleRadians - this.startAngleRadians) / (expandButtonCount - 1);
26 | regulateAverageAngle(endAngleRadians, expandButtonCount);
27 | }
28 | }
29 |
30 | /**
31 | * @param radius the sum of main button radius and sub button radius and the px value of attribute aebButtonGapDp
32 | * @param buttonIndex button index, count from startAngle to endAngle, value is 1 to expandButtonCount
33 | * @return the px distance in x direction that the button should move when expand
34 | */
35 | public int getMoveX(int radius, int buttonIndex) {
36 | double angle = getCurrentAngle(buttonIndex);
37 | int moveX;
38 | if (averageAngleRadians == 0) {
39 | moveX = (int)(Math.cos(angle) * radius) * buttonIndex;
40 | } else {
41 | moveX = (int)(Math.cos(angle) * radius);
42 | }
43 | return moveX;
44 | }
45 |
46 | public int getMoveY(int radius, int buttonIndex) {
47 | double angle = getCurrentAngle(buttonIndex);
48 | int moveY;
49 | if (averageAngleRadians == 0) {
50 | moveY = (int)(Math.sin(angle) * radius) * buttonIndex;
51 | } else {
52 | moveY = (int)(Math.sin(angle) * radius);
53 | }
54 | return moveY;
55 | }
56 |
57 | /**
58 | * regulate averageAngleRadians if endAngleDegree - startAngleDegree = 360 to avoid the first button covers the last button
59 | * @param endAngleRadians end angle in radians unit
60 | * @param expandButtonCount the count of buttons that will expand
61 | */
62 | private void regulateAverageAngle(double endAngleRadians, int expandButtonCount) {
63 | if (!angleStartEqualsEnd && startAngleRadians == endAngleRadians) {
64 | double tmp = 2 * Math.PI / expandButtonCount;
65 | if (averageAngleRadians < 0) {
66 | averageAngleRadians = -tmp;
67 | } else {
68 | averageAngleRadians = tmp;
69 | }
70 | }
71 | }
72 |
73 | /**
74 | * @param buttonIndex button index, count from startAngle to endAngle, value is 1 to expandButtonCount
75 | * @return the angle from first button to the buttonIndex button
76 | */
77 | private double getCurrentAngle(int buttonIndex) {
78 | return startAngleRadians + averageAngleRadians * (buttonIndex - 1);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
31 |
32 |
50 |
51 |
70 |
71 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/library/src/main/java/com/rance/library/ButtonData.java:
--------------------------------------------------------------------------------
1 | package com.rance.library;
2 |
3 | import android.content.Context;
4 | import android.graphics.Color;
5 | import android.graphics.drawable.Drawable;
6 |
7 | /**
8 | * 作者:Rance on 2016/11/10 16:41
9 | * 邮箱:rance935@163.com
10 | */
11 | public class ButtonData implements Cloneable {
12 | private static final int DEFAULT_BACKGROUND_COLOR = Color.WHITE;
13 |
14 | private boolean isMainButton = false;//main button is the button you see when buttons are all collapsed
15 | private boolean iconButton;//true if the button use icon resource,else string resource
16 |
17 | private String[] texts;//String array that you want to show at button center,texts[i] will be shown at the ith row
18 | private Drawable icon;//icon drawable that will be shown at button center
19 | private float iconPaddingDp;//the padding of the icon drawable in button
20 | private int backgroundColor = DEFAULT_BACKGROUND_COLOR;//the background color of the button
21 |
22 | @Override
23 | protected Object clone() throws CloneNotSupportedException {
24 | ButtonData buttonData = (ButtonData)super.clone();
25 | buttonData.setIsIconButton(this.iconButton);
26 | buttonData.setBackgroundColor(this.backgroundColor);
27 | buttonData.setIsMainButton(this.isMainButton);
28 | buttonData.setIcon(this.icon);
29 | buttonData.setIconPaddingDp(this.iconPaddingDp);
30 | buttonData.setTexts(this.texts);
31 | return buttonData;
32 | }
33 |
34 | public static ButtonData buildTextButton(String... text) {
35 | ButtonData buttonData = new ButtonData(false);
36 | buttonData.iconButton = false;
37 | buttonData.setText(text);
38 | return buttonData;
39 | }
40 |
41 | public static ButtonData buildIconButton(Context context, int iconResId, float iconPaddingDp) {
42 | ButtonData buttonData = new ButtonData(true);
43 | buttonData.iconButton = true;
44 | buttonData.iconPaddingDp = iconPaddingDp;
45 | buttonData.setIconResId(context, iconResId);
46 | return buttonData;
47 | }
48 |
49 | private ButtonData(boolean iconButton) {
50 | this.iconButton = iconButton;
51 | }
52 |
53 | public void setIsMainButton(boolean isMainButton) {
54 | this.isMainButton = isMainButton;
55 | }
56 |
57 | public boolean isMainButton() {
58 | return isMainButton;
59 | }
60 |
61 | public void setIsIconButton(boolean isIconButton) {
62 | iconButton = isIconButton;
63 | }
64 |
65 | public String[] getTexts() {
66 | return texts;
67 | }
68 |
69 | public void setTexts(String[] texts) {
70 | this.texts = texts;
71 | }
72 |
73 | public void setText(String... text) {
74 | this.texts = new String[text.length];
75 | for (int i = 0, length = text.length; i < length; i++) {
76 | this.texts[i] = text[i];
77 | }
78 | }
79 |
80 | public void setIcon(Drawable icon) {
81 | this.icon = icon;
82 | }
83 |
84 | public Drawable getIcon() {
85 | return this.icon;
86 | }
87 |
88 | public void setIconResId(Context context, int iconResId) {
89 | this.icon = context.getResources().getDrawable(iconResId);
90 | }
91 |
92 | public boolean isIconButton() {
93 | return iconButton;
94 | }
95 |
96 | public float getIconPaddingDp() {
97 | return iconPaddingDp;
98 | }
99 |
100 | public void setIconPaddingDp(float padding) {
101 | this.iconPaddingDp = padding;
102 | }
103 |
104 | public int getBackgroundColor() {
105 | return backgroundColor;
106 | }
107 |
108 | public void setBackgroundColor(int backgroundColor) {
109 | this.backgroundColor = backgroundColor;
110 | }
111 |
112 | public void setBackgroundColorId(Context context, int backgroundColorId) {
113 | this.backgroundColor = context.getResources().getColor(backgroundColorId);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SectorMenu效果演示
2 | 
3 | # 如何使用
4 | * 布局
5 | ```
6 |
23 | ```
24 | * 初始化菜单
25 | ```
26 | private void initSectorMenuButton() {
27 | SectorMenuButton sectorMenuButton = (SectorMenuButton) findViewById(R.id.sector_menu);
28 | final List buttonDatas = new ArrayList<>();
29 | int[] drawable = {R.mipmap.like, R.mipmap.mark, R.mipmap.search, R.mipmap.copy};
30 | for (int i = 0; i < 4; i++) {
31 | //最后一个参数表示padding
32 | ButtonData buttonData = ButtonData.buildIconButton(this, drawable[i], 0);
33 | buttonData.setBackgroundColorId(this, R.color.colorAccent);
34 | buttonDatas.add(buttonData);
35 | }
36 | sectorMenuButton.setButtonDatas(buttonDatas);
37 | setListener(sectorMenuButton);
38 | }
39 | ```
40 | * 设置监听
41 | ```
42 | private void setListener(final SectorMenuButton button) {
43 | button.setButtonEventListener(new ButtonEventListener() {
44 | @Override
45 | public void onButtonClicked(int index) {
46 | showToast("button" + index);
47 | }
48 | @Override
49 | public void onExpand() {
50 | showToast("onExpand");
51 | }
52 | @Override
53 | public void onCollapse() {
54 | showToast("onCollapse");
55 | }
56 | });
57 | }
58 | ```
59 | # 自定义属性说明
60 | * aebStartAngleDegree 展开按钮的开始角度
61 | * aebEndAngleDegree 展开按钮的结束角度
62 | * aebMaskBackgroundColor 当菜单为开启状态时全屏的背景颜色
63 | * aebIsSelectionMode 当子按钮被选中,主按钮设置为选定状态
64 | * aebAnimDurationMillis 开启关闭菜单时播放动画的时间
65 | * aebMainButtonRotateAnimDurationMillis 主按钮旋转动画的时间
66 | * aebMainButtonRotateDegree 主按钮旋转动画的角度
67 | * aebButtonElevation 按钮阴影效果的范围
68 | * aebRippleEffect 当按钮单点击时是否设置水波纹效果
69 | * aebRippleColor 主按钮点击时水波纹效果的颜色,默认为按钮颜色
70 | * aebMainButtonSizeDp 主按钮的大小
71 | * aebMainButtonTextSizeSp 主按钮的文字大小
72 | * aebMainButtonTextColor 主按钮的文字颜色
73 | * aebSubButtonSizeDp 子按钮的大小
74 | * aebSubButtonTextSizeSp 子按钮的文字大小
75 | * aebSubButtonTextColor 子按钮文字颜色
76 | * aebButtonGapDp 主按钮与子按钮之间的距离
77 |
78 | # 其它说明
79 | 因为当中用到了高斯模糊效果,代码如下
80 | ```
81 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
82 | @WorkerThread
83 | private Bitmap getBlurBitmap(Context context, Bitmap inBitmap, float radius) {
84 | if (context == null || inBitmap == null) {
85 | throw new IllegalArgumentException("have not called setParams() before call execute()");
86 | }
87 | int width = Math.round(inBitmap.getWidth() * SCALE);
88 | int height = Math.round(inBitmap.getHeight() * SCALE);
89 | Bitmap in = Bitmap.createScaledBitmap(inBitmap, width, height, false);
90 | Bitmap out = Bitmap.createBitmap(in);
91 | RenderScript rs = RenderScript.create(context);
92 | ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs,Element.U8_4(rs));
93 | Allocation allocationIn = Allocation.createFromBitmap(rs, in);
94 | Allocation allocationOut = Allocation.createFromBitmap(rs, out);
95 | blurScript.setRadius(radius);
96 | blurScript.setInput(allocationIn);
97 | blurScript.forEach(allocationOut);
98 | allocationOut.copyTo(out);
99 | allocationIn.destroy();
100 | allocationOut.destroy();
101 | blurScript.destroy();
102 | rs.destroy();
103 | return out;
104 | }
105 | ```
106 | Android Studio中,support v8包不能通过添加dependencies的方式加入。从sdk的目录下手动拷贝到工程的libs下面。这个包的参考路径如下:sdk/build-tools/21.1.1/renderscript/lib/renderscript-v8.jar。
107 | 但是有个更简单的方法,只要在build.gradle中加入下面两句话即可直接使用RenderScript相关API:
108 | ```
109 | defaultConfig {
110 | ...
111 | //start
112 | renderscriptTargetApi 18
113 | renderscriptSupportModeEnabled true
114 | //end
115 | }
116 | ```
117 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rance/sectormenu/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.rance.sectormenu;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.widget.Toast;
6 |
7 | import com.rance.library.ButtonData;
8 | import com.rance.library.ButtonEventListener;
9 | import com.rance.library.SectorMenuButton;
10 |
11 | import java.util.ArrayList;
12 | import java.util.List;
13 |
14 | /**
15 | * 作者:Rance on 2016/11/10 16:41
16 | * 邮箱:rance935@163.com
17 | */
18 | public class MainActivity extends AppCompatActivity {
19 |
20 | @Override
21 | protected void onCreate(Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 | setContentView(R.layout.activity_main);
24 | initBottomSectorMenuButton();
25 | initTopSectorMenuButton();
26 | initRightSectorMenuButton();
27 | initCenterSectorMenuButton();
28 | }
29 |
30 | private void initTopSectorMenuButton() {
31 | SectorMenuButton sectorMenuButton = (SectorMenuButton) findViewById(R.id.top_sector_menu);
32 | final List buttonDatas = new ArrayList<>();
33 | int[] drawable = {R.mipmap.like, R.mipmap.mark,
34 | R.mipmap.search, R.mipmap.copy};
35 | for (int i = 0; i < 4; i++) {
36 | ButtonData buttonData = ButtonData.buildIconButton(this, drawable[i], 0);
37 | buttonData.setBackgroundColorId(this, R.color.colorAccent);
38 | buttonDatas.add(buttonData);
39 | }
40 | sectorMenuButton.setButtonDatas(buttonDatas);
41 | setListener(sectorMenuButton);
42 | }
43 |
44 | private void initRightSectorMenuButton() {
45 | SectorMenuButton sectorMenuButton = (SectorMenuButton) findViewById(R.id.right_sector_menu);
46 | final List buttonDatas = new ArrayList<>();
47 | int[] drawable = {R.mipmap.like, R.mipmap.mark,
48 | R.mipmap.search, R.mipmap.copy};
49 | for (int i = 0; i < 4; i++) {
50 | ButtonData buttonData = ButtonData.buildIconButton(this, drawable[i], 0);
51 | buttonData.setBackgroundColorId(this, R.color.colorAccent);
52 | buttonDatas.add(buttonData);
53 | }
54 | sectorMenuButton.setButtonDatas(buttonDatas);
55 | setListener(sectorMenuButton);
56 | }
57 |
58 | private void initCenterSectorMenuButton() {
59 | SectorMenuButton sectorMenuButton = (SectorMenuButton) findViewById(R.id.center_sector_menu);
60 | final List buttonDatas = new ArrayList<>();
61 | int[] drawable = {R.mipmap.like, R.mipmap.mark,
62 | R.mipmap.search, R.mipmap.copy, R.mipmap.settings,
63 | R.mipmap.heart, R.mipmap.info, R.mipmap.record,
64 | R.mipmap.refresh};
65 | for (int i = 0; i < 9; i++) {
66 | ButtonData buttonData = ButtonData.buildIconButton(this, drawable[i], 0);
67 | buttonData.setBackgroundColorId(this, R.color.colorAccent);
68 | buttonDatas.add(buttonData);
69 | }
70 | sectorMenuButton.setButtonDatas(buttonDatas);
71 | setListener(sectorMenuButton);
72 | }
73 |
74 | private void initBottomSectorMenuButton() {
75 | SectorMenuButton sectorMenuButton = (SectorMenuButton) findViewById(R.id.bottom_sector_menu);
76 | final List buttonDatas = new ArrayList<>();
77 | int[] drawable = {R.mipmap.like, R.mipmap.mark,
78 | R.mipmap.search, R.mipmap.copy};
79 | for (int i = 0; i < 4; i++) {
80 | ButtonData buttonData = ButtonData.buildIconButton(this, drawable[i], 0);
81 | buttonData.setBackgroundColorId(this, R.color.colorAccent);
82 | buttonDatas.add(buttonData);
83 | }
84 | sectorMenuButton.setButtonDatas(buttonDatas);
85 | setListener(sectorMenuButton);
86 | }
87 |
88 | private void setListener(final SectorMenuButton button) {
89 | button.setButtonEventListener(new ButtonEventListener() {
90 | @Override
91 | public void onButtonClicked(int index) {
92 | showToast("button" + index);
93 | }
94 |
95 | @Override
96 | public void onExpand() {
97 | showToast("onExpand");
98 | }
99 |
100 | @Override
101 | public void onCollapse() {
102 | showToast("onCollapse");
103 | }
104 | });
105 | }
106 |
107 | private void showToast(String text) {
108 | Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show();
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/src/main/java/com/rance/library/SectorMenuButton.java:
--------------------------------------------------------------------------------
1 | package com.rance.library;
2 |
3 | import android.animation.Animator;
4 | import android.animation.ObjectAnimator;
5 | import android.animation.ValueAnimator;
6 | import android.annotation.SuppressLint;
7 | import android.content.Context;
8 | import android.content.res.TypedArray;
9 | import android.graphics.Bitmap;
10 | import android.graphics.Canvas;
11 | import android.graphics.Color;
12 | import android.graphics.Matrix;
13 | import android.graphics.Paint;
14 | import android.graphics.Path;
15 | import android.graphics.PointF;
16 | import android.graphics.RadialGradient;
17 | import android.graphics.Rect;
18 | import android.graphics.RectF;
19 | import android.graphics.Shader;
20 | import android.graphics.drawable.Drawable;
21 | import android.support.annotation.IntDef;
22 | import android.support.v4.graphics.ColorUtils;
23 | import android.util.AttributeSet;
24 | import android.view.MotionEvent;
25 | import android.view.View;
26 | import android.view.ViewGroup;
27 | import android.view.ViewTreeObserver;
28 | import android.view.animation.AnticipateInterpolator;
29 | import android.view.animation.Interpolator;
30 | import android.view.animation.OvershootInterpolator;
31 | import android.widget.ImageView;
32 |
33 | import java.lang.annotation.Retention;
34 | import java.lang.annotation.RetentionPolicy;
35 | import java.util.ArrayList;
36 | import java.util.HashMap;
37 | import java.util.List;
38 | import java.util.Map;
39 |
40 | /**
41 | * 作者:Rance on 2016/11/10 16:41
42 | * 邮箱:rance935@163.com
43 | */
44 | public class SectorMenuButton extends View implements ValueAnimator.AnimatorUpdateListener {
45 | private List buttonDatas;
46 | private Map buttonRects;
47 | private ButtonEventListener buttonEventListener;
48 |
49 | private static final int BUTTON_SHADOW_COLOR = 0xff000000;
50 | private static final int BUTTON_SHADOW_ALPHA = 32;
51 |
52 | //初始化ButtonData默认值
53 | private static final int DEFAULT_EXPAND_ANIMATE_DURATION = 225;
54 | private static final int DEFAULT_ROTATE_ANIMATE_DURATION = 300;
55 | private static final int DEFAULT_BUTTON_GAP_DP = 25;
56 | private static final int DEFAULT_BUTTON_MAIN_SIZE_DP = 60;
57 | private static final int DEFAULT_BUTTON_SUB_SIZE_DP = 60;
58 | private static final int DEFAULT_BUTTON_ELEVATION_DP = 4;
59 | private static final int DEFAULT_BUTTON_TEXT_SIZE_SP = 20;
60 | private static final int DEFAULT_START_ANGLE = 90;
61 | private static final int DEFAULT_END_ANGLE = 90;
62 | private static final int DEFAULT_BUTTON_TEXT_COLOR = Color.BLACK;
63 | private static final int DEFAULT_MASK_BACKGROUND_COLOR = Color.TRANSPARENT;
64 | private static final int DEFAULT_BLUR_RADIUS = 10;
65 |
66 | private boolean expanded = false;
67 |
68 | private float startAngle;
69 | private float endAngle;
70 | private int buttonGapPx;
71 | private int mainButtonRotateDegree;
72 | private int rotateAnimDuration;
73 | private int mainButtonSizePx;
74 | private int subButtonSizePx;
75 | private int mainButtonTextSize;
76 | private int subButtonTextSize;
77 | private int mainButtonTextColor;
78 | private int subButtonTextColor;
79 | private int expandAnimDuration;
80 | private int maskBackgroundColor;
81 | private int buttonElevationPx;
82 | private boolean isSelectionMode;
83 | private boolean rippleEffect;
84 | private int rippleColor = Integer.MIN_VALUE;
85 | private boolean blurBackground;
86 | private float blurRadius;
87 |
88 | private Bitmap mainShadowBitmap = null;
89 | private Bitmap subShadowBitmap = null;
90 | Matrix shadowMatrix;
91 |
92 | private int buttonSideMarginPx;
93 |
94 | private Paint paint;
95 | private Paint textPaint;
96 |
97 | private AngleCalculator angleCalculator;
98 | private boolean animating = false;
99 | private boolean maskAttached = false;
100 | private float expandProgress;
101 | private float rotateProgress;
102 | private ValueAnimator expandValueAnimator;
103 | private ValueAnimator collapseValueAnimator;
104 | private ValueAnimator rotateValueAnimator;
105 | private Interpolator overshootInterpolator;
106 | private Interpolator anticipateInterpolator;
107 | private Path ripplePath;
108 | private RippleInfo rippleInfo;
109 | private MaskView maskView;
110 | private Blur blur;
111 | private ImageView blurImageView;
112 | private ObjectAnimator blurAnimator;
113 | private Animator.AnimatorListener blurListener;
114 | private PointF pressPointF;
115 | private Rect rawButtonRect;
116 | private RectF rawButtonRectF;
117 | private int pressTmpColor;
118 | private boolean pressInButton;
119 |
120 | private QuickClickChecker checker;
121 | private int checkThreshold;
122 |
123 | private static class RippleInfo {
124 | float pressX;
125 | float pressY;
126 | float rippleRadius;
127 | int buttonIndex;
128 | int rippleColor = Integer.MIN_VALUE;
129 | }
130 |
131 | public SectorMenuButton(Context context) {
132 | this(context, null);
133 | }
134 |
135 | public SectorMenuButton(Context context, AttributeSet attrs) {
136 | this(context, attrs, 0);
137 | }
138 |
139 | public SectorMenuButton(Context context, AttributeSet attrs, int defStyleAttr) {
140 | super(context, attrs, defStyleAttr);
141 | init(context, attrs);
142 | }
143 |
144 | /**
145 | * 初始化
146 | * @param context
147 | * @param attrs
148 | */
149 | private void init(Context context, AttributeSet attrs) {
150 | paint = new Paint();
151 | paint.setAntiAlias(true);
152 | paint.setStyle(Paint.Style.FILL);
153 |
154 | //得到XML自定义属性
155 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SectorMenuButton);
156 | startAngle = ta.getInteger(R.styleable.SectorMenuButton_aebStartAngleDegree, DEFAULT_START_ANGLE);
157 | endAngle = ta.getInteger(R.styleable.SectorMenuButton_aebEndAngleDegree, DEFAULT_END_ANGLE);
158 |
159 | buttonGapPx = ta.getDimensionPixelSize(R.styleable.SectorMenuButton_aebButtonGapDp, dp2px(context, DEFAULT_BUTTON_GAP_DP));
160 | mainButtonSizePx = ta.getDimensionPixelSize(R.styleable.SectorMenuButton_aebMainButtonSizeDp, dp2px(context, DEFAULT_BUTTON_MAIN_SIZE_DP));
161 | subButtonSizePx = ta.getDimensionPixelSize(R.styleable.SectorMenuButton_aebSubButtonSizeDp, dp2px(context, DEFAULT_BUTTON_SUB_SIZE_DP));
162 | buttonElevationPx = ta.getDimensionPixelSize(R.styleable.SectorMenuButton_aebButtonElevation, dp2px(context, DEFAULT_BUTTON_ELEVATION_DP));
163 | buttonSideMarginPx = buttonElevationPx * 2;
164 | mainButtonTextSize = ta.getDimensionPixelSize(R.styleable.SectorMenuButton_aebMainButtonTextSizeSp, sp2px(context, DEFAULT_BUTTON_TEXT_SIZE_SP));
165 | subButtonTextSize = ta.getDimensionPixelSize(R.styleable.SectorMenuButton_aebSubButtonTextSizeSp, sp2px(context, DEFAULT_BUTTON_TEXT_SIZE_SP));
166 | mainButtonTextColor = ta.getColor(R.styleable.SectorMenuButton_aebMainButtonTextColor, DEFAULT_BUTTON_TEXT_COLOR);
167 | subButtonTextColor = ta.getColor(R.styleable.SectorMenuButton_aebSubButtonTextColor, DEFAULT_BUTTON_TEXT_COLOR);
168 |
169 | expandAnimDuration = ta.getInteger(R.styleable.SectorMenuButton_aebAnimDurationMillis, DEFAULT_EXPAND_ANIMATE_DURATION);
170 | rotateAnimDuration = ta.getInteger(R.styleable.SectorMenuButton_aebMainButtonRotateAnimDurationMillis, DEFAULT_ROTATE_ANIMATE_DURATION);
171 | maskBackgroundColor = ta.getInteger(R.styleable.SectorMenuButton_aebMaskBackgroundColor, DEFAULT_MASK_BACKGROUND_COLOR);
172 | mainButtonRotateDegree = ta.getInteger(R.styleable.SectorMenuButton_aebMainButtonRotateDegree, mainButtonRotateDegree);
173 | isSelectionMode = ta.getBoolean(R.styleable.SectorMenuButton_aebIsSelectionMode, false);
174 | rippleEffect = ta.getBoolean(R.styleable.SectorMenuButton_aebRippleEffect, true);
175 | rippleColor = ta.getColor(R.styleable.SectorMenuButton_aebRippleColor, rippleColor);
176 | blurBackground = ta.getBoolean(R.styleable.SectorMenuButton_aebBlurBackground, false);
177 | blurRadius = ta.getFloat(R.styleable.SectorMenuButton_aebBlurRadius, DEFAULT_BLUR_RADIUS);
178 | ta.recycle();
179 |
180 | //模糊处理
181 | if (blurBackground) {
182 | blur = new Blur();
183 | blurImageView = new ImageView(getContext());
184 | }
185 |
186 | if (mainButtonRotateDegree != 0) {
187 | checkThreshold = expandAnimDuration > rotateAnimDuration ? expandAnimDuration : rotateAnimDuration;
188 | } else {
189 | checkThreshold = expandAnimDuration;
190 | }
191 | checker = new QuickClickChecker(checkThreshold);
192 |
193 | rippleInfo = new RippleInfo();
194 | pressPointF = new PointF();
195 | rawButtonRect = new Rect();
196 | rawButtonRectF = new RectF();
197 | shadowMatrix = new Matrix();
198 |
199 | initViewTreeObserver();
200 | initAnimators();
201 | }
202 |
203 | /**
204 | * 注册监听视图树的观察者(observer)
205 | */
206 | private void initViewTreeObserver() {
207 | ViewTreeObserver observer = getViewTreeObserver();
208 | observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
209 | @Override
210 | public void onGlobalLayout() {
211 | getGlobalVisibleRect(rawButtonRect);
212 | rawButtonRectF.set(rawButtonRect.left, rawButtonRect.top, rawButtonRect.right, rawButtonRect.bottom);
213 | }
214 | });
215 | }
216 |
217 | /**
218 | * 初始化动画
219 | */
220 | private void initAnimators() {
221 | overshootInterpolator = new OvershootInterpolator();
222 | anticipateInterpolator = new AnticipateInterpolator();
223 |
224 | //打开菜单动画
225 | expandValueAnimator = ValueAnimator.ofFloat(0, 1);
226 | expandValueAnimator.setDuration(expandAnimDuration);
227 | expandValueAnimator.setInterpolator(overshootInterpolator);
228 | expandValueAnimator.addUpdateListener(this);
229 | expandValueAnimator.addListener(new SimpleAnimatorListener() {
230 | @Override
231 | public void onAnimationStart(Animator animator) {
232 | animating = true;
233 | attachMask();
234 | }
235 |
236 | @Override
237 | public void onAnimationEnd(Animator animator) {
238 | animating = false;
239 | expanded = true;
240 | }
241 | });
242 |
243 | //关闭菜单动画
244 | collapseValueAnimator = ValueAnimator.ofFloat(1, 0);
245 | collapseValueAnimator.setDuration(expandAnimDuration);
246 | collapseValueAnimator.setInterpolator(anticipateInterpolator);
247 | collapseValueAnimator.addUpdateListener(this);
248 | collapseValueAnimator.addListener(new SimpleAnimatorListener() {
249 | @Override
250 | public void onAnimationStart(Animator animator) {
251 | animating = true;
252 | hideBlur();
253 | maskView.reset();
254 | }
255 |
256 | @Override
257 | public void onAnimationEnd(Animator animator) {
258 | animating = false;
259 | expanded = false;
260 | if (rotateValueAnimator == null) {
261 | detachMask();
262 | } else {
263 | if (expandAnimDuration >= rotateAnimDuration) {
264 | detachMask();
265 | }
266 | }
267 | }
268 | });
269 |
270 | if (mainButtonRotateDegree == 0) {
271 | return;
272 | }
273 |
274 | //主菜单旋转动画
275 | rotateValueAnimator = ValueAnimator.ofFloat(0, 1);
276 | rotateValueAnimator.setDuration(rotateAnimDuration);
277 | rotateValueAnimator.addUpdateListener(this);
278 | rotateValueAnimator.addListener(new SimpleAnimatorListener() {
279 | @Override
280 | public void onAnimationEnd(Animator animator) {
281 | if (!expanded && expandAnimDuration < rotateAnimDuration) {
282 | detachMask();
283 | }
284 | }
285 | });
286 | }
287 |
288 | //所有按钮的监听事件
289 | public void setButtonEventListener(ButtonEventListener listener) {
290 | buttonEventListener = listener;
291 | }
292 |
293 | //设置打开菜单的插值器
294 | public void setExpandAnimatorInterpolator(Interpolator interpolator) {
295 | if (interpolator != null) {
296 | expandValueAnimator.setInterpolator(interpolator);
297 | }
298 | }
299 |
300 | //设置关闭菜单的插值器
301 | public void setCollapseAnimatorInterpolator(Interpolator interpolator) {
302 | if (interpolator != null) {
303 | collapseValueAnimator.setInterpolator(interpolator);
304 | }
305 | }
306 |
307 | //按钮初始化
308 | public SectorMenuButton setButtonDatas(List buttonDatas) {
309 | if (buttonDatas == null || buttonDatas.isEmpty()) {
310 | return this;
311 | }
312 | this.buttonDatas = new ArrayList<>(buttonDatas);
313 | if (isSelectionMode) {
314 | try {
315 | this.buttonDatas.add(0, (ButtonData) buttonDatas.get(0).clone());
316 | } catch (CloneNotSupportedException e) {
317 | e.printStackTrace();
318 | }
319 | }
320 |
321 | buttonRects = new HashMap<>(this.buttonDatas.size());
322 | for (int i = 0, size = this.buttonDatas.size(); i < size; i++) {
323 | ButtonData buttonData = this.buttonDatas.get(i);
324 | buttonData.setIsMainButton(i == 0);
325 | int buttonSizePx = buttonData.isMainButton() ? mainButtonSizePx : subButtonSizePx;
326 | RectF rectF = new RectF(buttonSideMarginPx, buttonSideMarginPx
327 | , buttonSizePx + buttonSideMarginPx, buttonSizePx + buttonSideMarginPx);
328 | buttonRects.put(buttonData, rectF);
329 | }
330 | angleCalculator = new AngleCalculator(startAngle, endAngle, this.buttonDatas.size() - 1);
331 | return this;
332 | }
333 |
334 | public List getButtonDatas() {
335 | return this.buttonDatas;
336 | }
337 |
338 | private ButtonData getMainButtonData() {
339 | return this.buttonDatas.get(0);
340 | }
341 |
342 | @Override
343 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
344 | int desiredWidth = mainButtonSizePx + buttonSideMarginPx * 2;
345 | int desiredHeight = mainButtonSizePx + buttonSideMarginPx * 2;
346 |
347 | setMeasuredDimension(desiredWidth, desiredHeight);
348 | }
349 |
350 | @Override
351 | protected void onDraw(Canvas canvas) {
352 | super.onDraw(canvas);
353 | drawButton(canvas);
354 | }
355 |
356 | @Override
357 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
358 | super.onSizeChanged(w, h, oldw, oldh);
359 | initButtonInfo();
360 | invalidate();
361 | }
362 |
363 | @Override
364 | public boolean onTouchEvent(MotionEvent event) {
365 | pressPointF.set(event.getRawX(), event.getRawY());
366 | switch (event.getAction()) {
367 | case MotionEvent.ACTION_DOWN:
368 | if (checker.isQuick()) {
369 | return false;
370 | }
371 | pressInButton = true;
372 | boolean executeActionUp = !animating && buttonDatas != null && !buttonDatas.isEmpty();
373 | if (executeActionUp) {
374 | updatePressState(0, true);
375 | }
376 | return executeActionUp;
377 | case MotionEvent.ACTION_MOVE:
378 | updatePressPosition(0, rawButtonRectF);
379 | break;
380 | case MotionEvent.ACTION_UP:
381 | if (!isPointInRectF(pressPointF, rawButtonRectF)) {
382 | return true;
383 | }
384 | updatePressState(0, false);
385 | expand();
386 | return true;
387 | }
388 | return super.onTouchEvent(event);
389 | }
390 |
391 | /**
392 | * 更新按钮位置
393 | * @param buttonIndex
394 | * @param rectF
395 | */
396 | private void updatePressPosition(int buttonIndex, RectF rectF) {
397 | if (buttonIndex < 0) {
398 | return;
399 | }
400 | if (isPointInRectF(pressPointF, rectF)) {
401 | if (!pressInButton) {
402 | updatePressState(buttonIndex, true);
403 | pressInButton = true;
404 | }
405 | } else {
406 | if (pressInButton) {
407 | updatePressState(buttonIndex, false);
408 | pressInButton = false;
409 | }
410 | }
411 | }
412 |
413 | private boolean isPointInRectF(PointF pointF, RectF rectF) {
414 | return pointF.x >= rectF.left && pointF.x <= rectF.right && pointF.y >= rectF.top && pointF.y <= rectF.bottom;
415 | }
416 |
417 | //更新按钮的状态
418 | private void updatePressState(int buttonIndex, boolean down) {
419 | if (buttonIndex < 0) {
420 | return;
421 | }
422 | ButtonData buttonData = buttonDatas.get(buttonIndex);
423 | if (down) {
424 | pressTmpColor = buttonData.getBackgroundColor();
425 | buttonData.setBackgroundColor(getPressedColor(pressTmpColor));
426 | } else {
427 | buttonData.setBackgroundColor(pressTmpColor);
428 | }
429 | if (expanded) {
430 | maskView.invalidate();
431 | } else {
432 | invalidate();
433 | }
434 | }
435 |
436 | @Override
437 | public void onAnimationUpdate(ValueAnimator valueAnimator) {
438 | if (valueAnimator == expandValueAnimator || valueAnimator == collapseValueAnimator) {
439 | expandProgress = (float) valueAnimator.getAnimatedValue();
440 | }
441 | if (valueAnimator == rotateValueAnimator) {
442 | rotateProgress = (float) valueAnimator.getAnimatedValue();
443 | }
444 | if (maskAttached) {
445 | maskView.updateButtons();
446 | maskView.invalidate();
447 | }
448 | }
449 |
450 | //打开菜单
451 | public void expand() {
452 | if (expandValueAnimator.isRunning()) {
453 | expandValueAnimator.cancel();
454 | }
455 | expandValueAnimator.start();
456 | startRotateAnimator(true);
457 | if (buttonEventListener != null) {
458 | buttonEventListener.onExpand();
459 | }
460 | }
461 |
462 | //关闭菜单
463 | public void collapse() {
464 | if (collapseValueAnimator.isRunning()) {
465 | collapseValueAnimator.cancel();
466 | }
467 | collapseValueAnimator.start();
468 | startRotateAnimator(false);
469 | if (buttonEventListener != null) {
470 | buttonEventListener.onCollapse();
471 | }
472 | }
473 |
474 | //主菜单旋转动画
475 | private void startRotateAnimator(boolean expand) {
476 | if (rotateValueAnimator != null) {
477 | if (rotateValueAnimator.isRunning()) {
478 | rotateValueAnimator.cancel();
479 | }
480 | if (expand) {
481 | rotateValueAnimator.setInterpolator(overshootInterpolator);
482 | rotateValueAnimator.setFloatValues(0, 1);
483 | } else {
484 | rotateValueAnimator.setInterpolator(anticipateInterpolator);
485 | rotateValueAnimator.setFloatValues(1, 0);
486 | }
487 | rotateValueAnimator.start();
488 | }
489 | }
490 |
491 | //更新底层view
492 | private void attachMask() {
493 | if (maskView == null) {
494 | maskView = new MaskView(getContext(), this);
495 | }
496 |
497 | if (!maskAttached && !showBlur()) {
498 | ViewGroup root = (ViewGroup) getRootView();
499 | root.addView(maskView);
500 | maskAttached = true;
501 | maskView.reset();
502 | maskView.initButtonRect();
503 | maskView.onClickMainButton();
504 | }
505 | }
506 |
507 | /**
508 | * 模糊处理
509 | * @return
510 | */
511 | private boolean showBlur() {
512 | if (!blurBackground) {
513 | return false;
514 | }
515 |
516 | setVisibility(INVISIBLE);
517 |
518 | final ViewGroup root = (ViewGroup) getRootView();
519 | root.setDrawingCacheEnabled(true);
520 | Bitmap bitmap = root.getDrawingCache();
521 | checkBlurRadius();
522 |
523 | blur.setParams(new Blur.Callback() {
524 | @Override
525 | public void onBlurred(Bitmap blurredBitmap) {
526 | blurImageView.setImageBitmap(blurredBitmap);
527 | root.setDrawingCacheEnabled(false);
528 | root.addView(blurImageView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
529 |
530 | blurAnimator = ObjectAnimator.ofFloat(blurImageView, "alpha", 0.0f, 1.0f).setDuration(expandAnimDuration);
531 | if (blurListener != null) {
532 | blurAnimator.removeListener(blurListener);
533 | }
534 | blurAnimator.start();
535 |
536 | root.addView(maskView);
537 | maskAttached = true;
538 | maskView.reset();
539 | maskView.initButtonRect();
540 | maskView.onClickMainButton();
541 | }
542 | }, getContext(), bitmap, blurRadius);
543 | blur.execute();
544 |
545 | return true;
546 | }
547 |
548 | /**
549 | * 检查模糊处理的Radius,必须在0~25之间
550 | */
551 | private void checkBlurRadius() {
552 | if (blurRadius <= 0 || blurRadius > 25) {
553 | blurRadius = DEFAULT_BLUR_RADIUS;
554 | }
555 | }
556 |
557 | /**
558 | * 隐藏模糊处理
559 | */
560 | private void hideBlur() {
561 | if (!blurBackground) {
562 | return;
563 | }
564 |
565 | setVisibility(VISIBLE);
566 |
567 | final ViewGroup root = (ViewGroup) getRootView();
568 | blurAnimator.setFloatValues(1.0f, 0.0f);
569 | if (blurListener == null) {
570 | blurListener = new SimpleAnimatorListener() {
571 | @Override
572 | public void onAnimationEnd(Animator animator) {
573 | root.removeView(blurImageView);
574 | }
575 | };
576 | }
577 | blurAnimator.addListener(blurListener);
578 | blurAnimator.start();
579 | }
580 |
581 | /**
582 | * 隐藏底层view
583 | */
584 | private void detachMask() {
585 | if (maskAttached) {
586 | ViewGroup root = (ViewGroup) getRootView();
587 | root.removeView(maskView);
588 | maskAttached = false;
589 | for (int i = 0; i < buttonDatas.size(); i++) {
590 | ButtonData buttonData = buttonDatas.get(i);
591 | RectF rectF = buttonRects.get(buttonData);
592 | int size = buttonData.isMainButton() ? mainButtonSizePx : subButtonSizePx;
593 | rectF.set(buttonSideMarginPx, buttonSideMarginPx, buttonSideMarginPx + size, buttonSideMarginPx + size);
594 | }
595 | }
596 | invalidate();
597 | }
598 |
599 | /**
600 | * 重置按钮水波纹效果
601 | */
602 | private void resetRippleInfo() {
603 | rippleInfo.buttonIndex = Integer.MIN_VALUE;
604 | rippleInfo.pressX = 0;
605 | rippleInfo.pressY = 0;
606 | rippleInfo.rippleRadius = 0;
607 | }
608 |
609 | /**
610 | * 绘制主菜单
611 | * @param canvas
612 | */
613 | private void drawButton(Canvas canvas) {
614 | if (buttonDatas == null || buttonDatas.isEmpty()) {
615 | return;
616 | }
617 |
618 | ButtonData buttonData = getMainButtonData();
619 | drawButton(canvas, paint, buttonData);
620 | }
621 |
622 | /**
623 | * 绘制指定按钮
624 | * @param canvas
625 | */
626 | private void drawButton(Canvas canvas, Paint paint, ButtonData buttonData) {
627 | drawShadow(canvas, paint, buttonData);
628 | drawContent(canvas, paint, buttonData);
629 | drawRipple(canvas, paint, buttonData);
630 | }
631 |
632 | /**
633 | * 绘制阴影效果
634 | * @param canvas
635 | * @param paint
636 | * @param buttonData
637 | */
638 | private void drawShadow(Canvas canvas, Paint paint, ButtonData buttonData) {
639 | if (buttonElevationPx <= 0) {
640 | return;
641 | }
642 |
643 | float left, top;
644 | Bitmap bitmap;
645 | if (buttonData.isMainButton()) {
646 | mainShadowBitmap = getButtonShadowBitmap(buttonData);
647 | bitmap = mainShadowBitmap;
648 | } else {
649 | subShadowBitmap = getButtonShadowBitmap(buttonData);
650 | bitmap = subShadowBitmap;
651 | }
652 |
653 | int shadowOffset = buttonElevationPx / 2;
654 | RectF rectF = buttonRects.get(buttonData);
655 | left = rectF.centerX() - bitmap.getWidth() / 2;
656 | top = rectF.centerY() - bitmap.getHeight() / 2 + shadowOffset;
657 | shadowMatrix.reset();
658 | if (!buttonData.isMainButton()) {
659 | shadowMatrix.postScale(expandProgress, expandProgress, bitmap.getWidth() / 2, bitmap.getHeight() / 2 + shadowOffset);
660 | }
661 | shadowMatrix.postTranslate(left, top);
662 | if (buttonData.isMainButton()) {
663 | shadowMatrix.postRotate(-mainButtonRotateDegree * rotateProgress, rectF.centerX(), rectF.centerY());
664 | }
665 | paint.setAlpha(255);
666 | canvas.drawBitmap(bitmap, shadowMatrix, paint);
667 | }
668 |
669 | /**
670 | * 绘制菜单按钮的内容(icon 文字)
671 | * @param canvas
672 | * @param paint
673 | * @param buttonData
674 | */
675 | private void drawContent(Canvas canvas, Paint paint, ButtonData buttonData) {
676 | paint.setAlpha(255);
677 | paint.setColor(buttonData.getBackgroundColor());
678 | RectF rectF = buttonRects.get(buttonData);
679 | canvas.drawOval(rectF, paint);
680 | if (buttonData.isIconButton()) {
681 | Drawable drawable = buttonData.getIcon();
682 | if (drawable == null) {
683 | throw new IllegalArgumentException("iconData is true, drawable cannot be null");
684 | }
685 | int left = (int) rectF.left + dp2px(getContext(), buttonData.getIconPaddingDp());
686 | int right = (int) rectF.right - dp2px(getContext(), buttonData.getIconPaddingDp());
687 | int top = (int) rectF.top + dp2px(getContext(), buttonData.getIconPaddingDp());
688 | int bottom = (int) rectF.bottom - dp2px(getContext(), buttonData.getIconPaddingDp());
689 | drawable.setBounds(left, top, right, bottom);
690 | drawable.draw(canvas);
691 | } else {
692 | if (buttonData.getTexts() == null) {
693 | throw new IllegalArgumentException("iconData is false, text cannot be null");
694 | }
695 | String[] texts = buttonData.getTexts();
696 | int sizePx = buttonData.isMainButton() ? mainButtonTextSize : subButtonTextSize;
697 | int textColor = buttonData.isMainButton() ? mainButtonTextColor : subButtonTextColor;
698 | textPaint = getTextPaint(sizePx, textColor);
699 | drawTexts(texts, canvas, rectF.centerX(), rectF.centerY());
700 | }
701 | }
702 |
703 | /**
704 | * 绘制文字
705 | * @param strings
706 | * @param canvas
707 | * @param x
708 | * @param y
709 | */
710 | private void drawTexts(String[] strings, Canvas canvas, float x, float y) {
711 | Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
712 | float top = fontMetrics.top;
713 | float bottom = fontMetrics.bottom;
714 | int length = strings.length;
715 | float total = (length - 1) * (-top + bottom) + (-fontMetrics.ascent + fontMetrics.descent);
716 | float offset = total / 2 - bottom;
717 | for (int i = 0; i < length; i++) {
718 | float yAxis = -(length - i - 1) * (-top + bottom) + offset;
719 | canvas.drawText(strings[i], x, y + yAxis, textPaint);
720 | }
721 | }
722 |
723 | /**
724 | * 绘制水波纹效果
725 | * @param canvas
726 | * @param paint
727 | * @param buttonData
728 | */
729 | private void drawRipple(Canvas canvas, Paint paint, ButtonData buttonData) {
730 | int pressIndex = buttonDatas.indexOf(buttonData);
731 | if (!rippleEffect || pressIndex == -1 || pressIndex != rippleInfo.buttonIndex) {
732 | return;
733 | }
734 |
735 | paint.setColor(rippleInfo.rippleColor);
736 | paint.setAlpha(128);
737 | canvas.save();
738 | if (ripplePath == null) {
739 | ripplePath = new Path();
740 | }
741 | ripplePath.reset();
742 | RectF rectF = buttonRects.get(buttonData);
743 | float radius = rectF.right - rectF.centerX();
744 | ripplePath.addCircle(rectF.centerX(), rectF.centerY(), radius, Path.Direction.CW);
745 | canvas.clipPath(ripplePath);
746 | canvas.drawCircle(rippleInfo.pressX, rippleInfo.pressY, rippleInfo.rippleRadius, paint);
747 | canvas.restore();
748 | }
749 |
750 | /**
751 | * 获取按钮阴影效果Bitmap
752 | * @param buttonData
753 | * @return
754 | */
755 | private Bitmap getButtonShadowBitmap(ButtonData buttonData) {
756 | if (buttonData.isMainButton()) {
757 | if (mainShadowBitmap != null) {
758 | return mainShadowBitmap;
759 | }
760 | } else {
761 | if (subShadowBitmap != null) {
762 | return subShadowBitmap;
763 | }
764 | }
765 |
766 | int buttonSizePx = buttonData.isMainButton() ? mainButtonSizePx : subButtonSizePx;
767 | int buttonRadius = buttonSizePx / 2;
768 | int bitmapRadius = buttonRadius + buttonElevationPx;
769 | int bitmapSize = bitmapRadius * 2;
770 | Bitmap bitmap = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888);
771 | bitmap.eraseColor(0x0);
772 | int colors[] = {ColorUtils.setAlphaComponent(BUTTON_SHADOW_COLOR, BUTTON_SHADOW_ALPHA),
773 | ColorUtils.setAlphaComponent(BUTTON_SHADOW_COLOR, 0)};
774 | float stops[] = {(float) (buttonRadius - buttonElevationPx) / (float) bitmapRadius, 1};
775 | Paint paint = new Paint();
776 | paint.setAntiAlias(true);
777 | paint.setShader(new RadialGradient(bitmapRadius, bitmapRadius, bitmapRadius, colors, stops, Shader.TileMode.CLAMP));
778 | Canvas canvas = new Canvas(bitmap);
779 | canvas.drawRect(0, 0, bitmapSize, bitmapSize, paint);
780 | if (buttonData.isMainButton()) {
781 | mainShadowBitmap = bitmap;
782 | return mainShadowBitmap;
783 | } else {
784 | subShadowBitmap = bitmap;
785 | return subShadowBitmap;
786 | }
787 | }
788 |
789 | private Paint getTextPaint(int sizePx, int color) {
790 | if (textPaint == null) {
791 | textPaint = new Paint();
792 | textPaint.setAntiAlias(true);
793 | textPaint.setTextAlign(Paint.Align.CENTER);
794 | }
795 |
796 | textPaint.setTextSize(sizePx);
797 | textPaint.setColor(color);
798 | return textPaint;
799 | }
800 |
801 | private void initButtonInfo() {
802 | ViewGroup root = (ViewGroup) getRootView();
803 | getGlobalVisibleRect(rawButtonRect);
804 | rawButtonRectF.set(rawButtonRect.left, rawButtonRect.top, rawButtonRect.right, rawButtonRect.bottom);
805 | }
806 |
807 | private int getLighterColor(int color) {
808 | float[] hsv = new float[3];
809 | Color.colorToHSV(color, hsv);
810 | hsv[2] *= 1.1f;
811 | return Color.HSVToColor(hsv);
812 | }
813 |
814 | private int getDarkerColor(int color) {
815 | float[] hsv = new float[3];
816 | Color.colorToHSV(color, hsv);
817 | hsv[2] *= 0.9f;
818 | return Color.HSVToColor(hsv);
819 | }
820 |
821 | private int getPressedColor(int color) {
822 | return getDarkerColor(color);
823 | }
824 |
825 | public int sp2px(Context context, float spValue) {
826 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
827 | return (int) (spValue * fontScale + 0.5f);
828 | }
829 |
830 | public int dp2px(Context context, float dpValue) {
831 | final float scale = context.getResources().getDisplayMetrics().density;
832 | return (int) (dpValue * scale + 0.5f);
833 | }
834 |
835 | @SuppressLint("ViewConstructor")
836 | private static class MaskView extends View {
837 | private SectorMenuButton SectorMenuButton;
838 | private RectF initialSubButtonRectF;
839 | private RectF touchRectF;
840 | private ValueAnimator touchRippleAnimator;
841 | private Paint paint;
842 | private Map expandDesCoordinateMap;
843 | private int rippleState;
844 | private float rippleRadius;
845 | private int clickIndex = 0;
846 | private Matrix[] matrixArray;
847 | private QuickClickChecker checker;
848 |
849 | private static final int IDLE = 0;
850 | private static final int RIPPLING = 1;
851 | private static final int RIPPLED = 2;
852 |
853 | @Retention(RetentionPolicy.SOURCE)
854 | @IntDef({IDLE, RIPPLING, RIPPLED})
855 | private @interface RippleState {
856 |
857 | }
858 |
859 | private static class ExpandMoveCoordinate {
860 | float moveX;
861 | float moveY;
862 |
863 | public ExpandMoveCoordinate(float moveX, float moveY) {
864 | this.moveX = moveX;
865 | this.moveY = moveY;
866 | }
867 | }
868 |
869 | public MaskView(Context context, SectorMenuButton button) {
870 | super(context);
871 | SectorMenuButton = button;
872 |
873 | checker = new QuickClickChecker(SectorMenuButton.checkThreshold);
874 |
875 | paint = new Paint();
876 | paint.setStyle(Paint.Style.FILL);
877 | paint.setAntiAlias(true);
878 |
879 | matrixArray = new Matrix[SectorMenuButton.buttonDatas.size()];
880 | for (int i = 0; i < matrixArray.length; i++) {
881 | matrixArray[i] = new Matrix();
882 | }
883 |
884 | initialSubButtonRectF = new RectF();
885 | touchRectF = new RectF();
886 |
887 | expandDesCoordinateMap = new HashMap<>(SectorMenuButton.buttonDatas.size());
888 | setBackgroundColor(SectorMenuButton.maskBackgroundColor);
889 |
890 | touchRippleAnimator = ValueAnimator.ofFloat(0, 1);
891 | touchRippleAnimator.setDuration((long) ((float) SectorMenuButton.expandAnimDuration * 0.9f));
892 | touchRippleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
893 | @Override
894 | public void onAnimationUpdate(ValueAnimator valueAnimator) {
895 | float animateProgress = (float) valueAnimator.getAnimatedValue();
896 | SectorMenuButton.rippleInfo.rippleRadius = rippleRadius * animateProgress;
897 | }
898 | });
899 | touchRippleAnimator.addListener(new SimpleAnimatorListener() {
900 | @Override
901 | public void onAnimationEnd(Animator animator) {
902 | SectorMenuButton.rippleInfo.rippleRadius = 0;
903 | setRippleState(RIPPLED);
904 | }
905 | });
906 | }
907 |
908 | @Override
909 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
910 | View root = getRootView();
911 | setMeasuredDimension(root.getWidth(), root.getHeight());
912 | }
913 |
914 | @Override
915 | protected void onDraw(Canvas canvas) {
916 | super.onDraw(canvas);
917 | drawButtons(canvas, paint);
918 | }
919 |
920 | @Override
921 | public boolean onTouchEvent(MotionEvent event) {
922 | SectorMenuButton.pressPointF.set(event.getX(), event.getY());
923 | switch (event.getAction()) {
924 | case MotionEvent.ACTION_DOWN:
925 | if (checker.isQuick()) {
926 | return false;
927 | }
928 | clickIndex = getTouchedButtonIndex();
929 | if (SectorMenuButton.expanded) {
930 | SectorMenuButton.updatePressState(clickIndex, true);
931 | }
932 | SectorMenuButton.pressInButton = true;
933 | return SectorMenuButton.expanded;
934 | case MotionEvent.ACTION_MOVE:
935 | SectorMenuButton.updatePressPosition(clickIndex, touchRectF);
936 | break;
937 | case MotionEvent.ACTION_UP:
938 | if (!SectorMenuButton.isPointInRectF(SectorMenuButton.pressPointF, touchRectF)) {
939 | if (clickIndex < 0) {
940 | SectorMenuButton.collapse();
941 | }
942 | return true;
943 | }
944 | SectorMenuButton.updatePressState(clickIndex, false);
945 | onButtonPressed();
946 | break;
947 | }
948 | return super.onTouchEvent(event);
949 | }
950 |
951 | private void reset() {
952 | setRippleState(IDLE);
953 | }
954 |
955 | private void setRippleState(@RippleState int state) {
956 | rippleState = state;
957 | }
958 |
959 | @RippleState
960 | private int getRippleState() {
961 | return rippleState;
962 | }
963 |
964 | public void onClickMainButton() {
965 | clickIndex = 0;
966 | }
967 |
968 | private void onButtonPressed() {
969 | if (SectorMenuButton.buttonEventListener != null) {
970 | if (clickIndex > 0) {
971 | SectorMenuButton.buttonEventListener.onButtonClicked(clickIndex);
972 | }
973 | }
974 | if (SectorMenuButton.isSelectionMode) {
975 | if (clickIndex > 0) {
976 | ButtonData buttonData = SectorMenuButton.buttonDatas.get(clickIndex);
977 | ButtonData mainButton = SectorMenuButton.getMainButtonData();
978 | if (buttonData.isIconButton()) {
979 | mainButton.setIsIconButton(true);
980 | mainButton.setIcon(buttonData.getIcon());
981 | } else {
982 | mainButton.setIsIconButton(false);
983 | mainButton.setTexts(buttonData.getTexts());
984 | }
985 | mainButton.setBackgroundColor(buttonData.getBackgroundColor());
986 | }
987 | }
988 | SectorMenuButton.collapse();
989 | }
990 |
991 | private int getTouchedButtonIndex() {
992 | for (int i = 0; i < SectorMenuButton.buttonDatas.size(); i++) {
993 | ButtonData buttonData = SectorMenuButton.buttonDatas.get(i);
994 | ExpandMoveCoordinate coordinate = expandDesCoordinateMap.get(buttonData);
995 | if (i == 0) {
996 | RectF rectF = SectorMenuButton.buttonRects.get(buttonData);
997 | touchRectF.set(rectF);
998 | } else {
999 | touchRectF.set(initialSubButtonRectF);
1000 | touchRectF.offset(coordinate.moveX, -coordinate.moveY);
1001 | }
1002 |
1003 | if (SectorMenuButton.isPointInRectF(SectorMenuButton.pressPointF, touchRectF)) {
1004 | return i;
1005 | }
1006 | }
1007 | return -1;
1008 | }
1009 |
1010 | private void initButtonRect() {
1011 | for (int i = 0; i < SectorMenuButton.buttonDatas.size(); i++) {
1012 | ButtonData buttonData = SectorMenuButton.buttonDatas.get(i);
1013 | RectF rectF = SectorMenuButton.buttonRects.get(buttonData);
1014 | if (i == 0) {
1015 | rectF.left = SectorMenuButton.rawButtonRectF.left + SectorMenuButton.buttonSideMarginPx;
1016 | rectF.right = SectorMenuButton.rawButtonRectF.right - SectorMenuButton.buttonSideMarginPx;
1017 | rectF.top = SectorMenuButton.rawButtonRectF.top + SectorMenuButton.buttonSideMarginPx;
1018 | rectF.bottom = SectorMenuButton.rawButtonRectF.bottom - SectorMenuButton.buttonSideMarginPx;
1019 | } else {
1020 | float leftTmp = rectF.left;
1021 | float topTmp = rectF.top;
1022 | int buttonRadius = SectorMenuButton.subButtonSizePx / 2;
1023 | rectF.left = leftTmp + SectorMenuButton.rawButtonRectF.centerX() - SectorMenuButton.buttonSideMarginPx - buttonRadius;
1024 | rectF.right = leftTmp + SectorMenuButton.rawButtonRectF.centerX() - SectorMenuButton.buttonSideMarginPx + buttonRadius;
1025 | rectF.top = topTmp + SectorMenuButton.rawButtonRectF.centerY() - SectorMenuButton.buttonSideMarginPx - buttonRadius;
1026 | rectF.bottom = topTmp + SectorMenuButton.rawButtonRectF.centerY() - SectorMenuButton.buttonSideMarginPx + buttonRadius;
1027 | initialSubButtonRectF.set(rectF);
1028 | touchRectF.set(rectF);
1029 | }
1030 | }
1031 | }
1032 |
1033 | private void updateButtons() {
1034 | List buttonDatas = SectorMenuButton.buttonDatas;
1035 | int mainButtonRadius = SectorMenuButton.mainButtonSizePx / 2;
1036 | int subButtonRadius = SectorMenuButton.subButtonSizePx / 2;
1037 | Matrix matrix = matrixArray[0];
1038 | matrix.reset();
1039 | matrix.postRotate(SectorMenuButton.mainButtonRotateDegree * SectorMenuButton.rotateProgress
1040 | , SectorMenuButton.rawButtonRectF.centerX(), SectorMenuButton.rawButtonRectF.centerY());
1041 | for (int i = 1; i < buttonDatas.size(); i++) {
1042 | matrix = matrixArray[i];
1043 | ButtonData buttonData = buttonDatas.get(i);
1044 | matrix.reset();
1045 | if (SectorMenuButton.expanded) {
1046 | ExpandMoveCoordinate coordinate = expandDesCoordinateMap.get(buttonData);
1047 | float dx = SectorMenuButton.expandProgress * (coordinate.moveX);
1048 | float dy = SectorMenuButton.expandProgress * (-coordinate.moveY);
1049 | matrix.postTranslate(dx, dy);
1050 | } else {
1051 | int radius = mainButtonRadius + subButtonRadius + SectorMenuButton.buttonGapPx;
1052 | float moveX;
1053 | float moveY;
1054 | ExpandMoveCoordinate coordinate = expandDesCoordinateMap.get(buttonData);
1055 | if (coordinate == null) {
1056 | moveX = SectorMenuButton.angleCalculator.getMoveX(radius, i);
1057 | moveY = SectorMenuButton.angleCalculator.getMoveY(radius, i);
1058 | coordinate = new ExpandMoveCoordinate(moveX, moveY);
1059 | expandDesCoordinateMap.put(buttonData, coordinate);
1060 | } else {
1061 | moveX = coordinate.moveX;
1062 | moveY = coordinate.moveY;
1063 | }
1064 | float dx = SectorMenuButton.expandProgress * (moveX);
1065 | float dy = SectorMenuButton.expandProgress * (-moveY);
1066 | matrix.postTranslate(dx, dy);
1067 | }
1068 | }
1069 | }
1070 |
1071 | private void drawButtons(Canvas canvas, Paint paint) {
1072 | for (int i = SectorMenuButton.buttonDatas.size() - 1; i >= 0; i--) {
1073 | canvas.save();
1074 | canvas.concat(matrixArray[i]);
1075 | ButtonData buttonData = SectorMenuButton.buttonDatas.get(i);
1076 | SectorMenuButton.drawButton(canvas, paint, buttonData);
1077 | canvas.restore();
1078 | }
1079 | }
1080 | }
1081 |
1082 | private static class SimpleAnimatorListener implements Animator.AnimatorListener {
1083 | @Override
1084 | public void onAnimationStart(Animator animator) {
1085 |
1086 | }
1087 |
1088 | @Override
1089 | public void onAnimationEnd(Animator animator) {
1090 |
1091 | }
1092 |
1093 | @Override
1094 | public void onAnimationCancel(Animator animator) {
1095 |
1096 | }
1097 |
1098 | @Override
1099 | public void onAnimationRepeat(Animator animator) {
1100 |
1101 | }
1102 | }
1103 | }
1104 |
--------------------------------------------------------------------------------