├── .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 | 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 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 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 | ![image](http://upload-images.jianshu.io/upload_images/1433157-0e103b7f4b41e928.gif?imageMogr2/auto-orient/strip) 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 | --------------------------------------------------------------------------------