├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── circle-menu-simple-example ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ramotion │ │ └── circlemenu │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── ramotion │ │ │ └── circlemenu │ │ │ └── example │ │ │ └── simple │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_home_white_24dp.png │ │ ├── ic_notifications_white_24dp.png │ │ ├── ic_place_white_24dp.png │ │ ├── ic_search_white_24dp.png │ │ └── ic_settings_white_24dp.png │ │ ├── drawable-mdpi │ │ ├── ic_home_white_24dp.png │ │ ├── ic_notifications_white_24dp.png │ │ ├── ic_place_white_24dp.png │ │ ├── ic_search_white_24dp.png │ │ └── ic_settings_white_24dp.png │ │ ├── drawable-xhdpi │ │ ├── ic_home_white_24dp.png │ │ ├── ic_notifications_white_24dp.png │ │ ├── ic_place_white_24dp.png │ │ ├── ic_search_white_24dp.png │ │ └── ic_settings_white_24dp.png │ │ ├── drawable-xxhdpi │ │ ├── ic_home_white_24dp.png │ │ ├── ic_notifications_white_24dp.png │ │ ├── ic_place_white_24dp.png │ │ ├── ic_search_white_24dp.png │ │ └── ic_settings_white_24dp.png │ │ ├── drawable-xxxhdpi │ │ ├── ic_home_white_24dp.png │ │ ├── ic_notifications_white_24dp.png │ │ ├── ic_place_white_24dp.png │ │ ├── ic_search_white_24dp.png │ │ └── ic_settings_white_24dp.png │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── buttons.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── ramotion │ └── circlemenu │ └── ExampleUnitTest.java ├── circle-menu ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ramotion │ │ └── circlemenu │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── ramotion │ │ │ └── circlemenu │ │ │ ├── CircleMenuView.java │ │ │ └── RingEffectView.java │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_close_black_24dp.png │ │ └── ic_menu_black_24dp.png │ │ ├── drawable-mdpi │ │ ├── ic_close_black_24dp.png │ │ └── ic_menu_black_24dp.png │ │ ├── drawable-xhdpi │ │ ├── ic_close_black_24dp.png │ │ └── ic_menu_black_24dp.png │ │ ├── drawable-xxhdpi │ │ ├── ic_close_black_24dp.png │ │ └── ic_menu_black_24dp.png │ │ ├── drawable-xxxhdpi │ │ ├── ic_close_black_24dp.png │ │ └── ic_menu_black_24dp.png │ │ ├── layout │ │ └── circle_menu.xml │ │ └── values │ │ ├── attrs.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── ramotion │ └── circlemenu │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── header.png ├── preview.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Java compiled classes # 2 | *.class 3 | 4 | # Package Files # 5 | *.apk 6 | *.ap_ 7 | 8 | # IDEA Project files # 9 | *.iws 10 | *.iml 11 | *.ipr 12 | .idea/ 13 | 14 | # Build directory # 15 | target/ 16 | build/ 17 | 18 | # Generated files # 19 | bin/ 20 | gen/ 21 | 22 | # Local props file # 23 | local.properties 24 | 25 | # Gradle cache # 26 | .gradle 27 | 28 | # OSX files # 29 | .DS_Store 30 | 31 | # NDK # 32 | obj/ 33 | 34 | # files for the dex VM # 35 | *.dex -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ramotion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

CIRCLE MENU [JAVA]

7 | 8 |

A simple, elegant UI menu with a circular layout and material design animations

9 | 10 | 11 | ___ 12 | 13 | 14 |

We specialize in the designing and coding of custom UI for Mobile Apps and Websites.
15 | 16 | 17 |

18 |

Stay tuned for the latest updates:
19 | 20 |

21 | 22 |
23 | 24 | [![Twitter](https://img.shields.io/badge/Twitter-@Ramotion-blue.svg?style=flat)](http://twitter.com/Ramotion) 25 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/42eb7b00b93645c0812c045ab26cb3b7)](https://www.codacy.com/app/dvg4000/circle-menu-android?utm_source=github.com&utm_medium=referral&utm_content=Ramotion/circle-menu-android&utm_campaign=Badge_Grade) 26 | [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/Ramotion) 27 | 28 | 29 | ## Requirements 30 | ​ 31 | - Android 4.1 Jelly Bean (API lvl 16) or greater 32 | - Your favorite IDE 33 | 34 | ## Installation 35 | ​ 36 | Just download the package from [here](http://central.maven.org/maven2/com/ramotion/circlemenu/circle-menu/0.3.2/circle-menu-0.3.2.aar) and add it to your project classpath, or just use the maven repo: 37 | 38 | Gradle: 39 | ```groovy 40 | implementation 'com.ramotion.circlemenu:circle-menu:0.3.2' 41 | ``` 42 | SBT: 43 | ```scala 44 | libraryDependencies += "com.ramotion.circlemenu" % "circle-menu" % "0.3.2" 45 | ``` 46 | Maven: 47 | ```xml 48 | 49 | com.ramotion.circlemenu 50 | circle-menu 51 | 0.3.2 52 | 53 | ``` 54 | ​ 55 | 56 | ## Basic usage 57 | 58 | Place the `CircleMenuView` in your layout and set the icons and colors of the buttons, as shown below. 59 | ```xml 60 | app:button_colors="@array/colors" 61 | app:button_icons="@array/icons" 62 | ``` 63 | 64 | Example of arrays `colors` and `icons` in `res\values\buttons.xml`: 65 | ```xml 66 | 67 | 68 | 69 | @drawable/ic_home_white_24dp 70 | @drawable/ic_search_white_24dp 71 | @drawable/ic_notifications_white_24dp 72 | @drawable/ic_settings_white_24dp 73 | @drawable/ic_place_white_24dp 74 | 75 | 76 | @android:color/holo_blue_light 77 | @android:color/holo_green_dark 78 | @android:color/holo_red_light 79 | @android:color/holo_purple 80 | @android:color/holo_orange_light 81 | 82 | 83 | ``` 84 | 85 | Or use the constructor 86 | ```java 87 | CircleMenuView(@NonNull Context context, @NonNull List icons, @NonNull List colors) 88 | ``` 89 | to add `CircleMenuView` and configure the buttons programmatically (in the code). 90 | 91 | Next, connect the event handler `CircleMenuView.EventListener` as shown below, 92 | and override the methods you need. 93 | 94 | ```java 95 | final CircleMenuView menu = (CircleMenuView) findViewById(R.id.circle_menu); 96 | menu.setEventListener(new CircleMenuView.EventListener() { 97 | @Override 98 | public void onMenuOpenAnimationStart(@NonNull CircleMenuView view) { 99 | Log.d("D", "onMenuOpenAnimationStart"); 100 | } 101 | 102 | @Override 103 | public void onMenuOpenAnimationEnd(@NonNull CircleMenuView view) { 104 | Log.d("D", "onMenuOpenAnimationEnd"); 105 | } 106 | 107 | @Override 108 | public void onMenuCloseAnimationStart(@NonNull CircleMenuView view) { 109 | Log.d("D", "onMenuCloseAnimationStart"); 110 | } 111 | 112 | @Override 113 | public void onMenuCloseAnimationEnd(@NonNull CircleMenuView view) { 114 | Log.d("D", "onMenuCloseAnimationEnd"); 115 | } 116 | 117 | @Override 118 | public void onButtonClickAnimationStart(@NonNull CircleMenuView view, int index) { 119 | Log.d("D", "onButtonClickAnimationStart| index: " + index); 120 | } 121 | 122 | @Override 123 | public void onButtonClickAnimationEnd(@NonNull CircleMenuView view, int index) { 124 | Log.d("D", "onButtonClickAnimationEnd| index: " + index); 125 | } 126 | }); 127 | ``` 128 | 129 | You can use `open(boolean animate)` and `close(boolean animate)` methods, 130 | to open and close menu programmatically 131 | 132 | Here are the attributes you can specify through XML or related setters: 133 | * `button_icons` - Array of buttons icons. 134 | * `button_colors` - Array of buttons colors. 135 | * `icon_menu` - Menu default icon. 136 | * `icon_close` - Menu closed icon. 137 | * `icon_color` - Menu icon color. 138 | * `duration_ring` - Ring effect duration. 139 | * `duration_open` - Menu opening animation duration. 140 | * `duration_close` - Menu closing animation duration. 141 | * `distance` - Distance between center button and buttons 142 | 143 |
144 | 145 | ## 🗂 Check this library on other language: 146 | 147 | 148 | 149 | 150 | 151 | 152 | ## 📄 License 153 | 154 | Circle Menu Android is released under the MIT license. 155 | See [LICENSE](./LICENSE) for details. 156 | 157 | This library is a part of a selection of our best UI open-source projects 158 | 159 | If you use the open-source library in your project, please make sure to credit and backlink to www.ramotion.com 160 | 161 | ## 📱 Get the Showroom App for Android to give it a try 162 | Try this UI component and more like this in our Android app. Contact us if interested. 163 | 164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.3.1' 10 | classpath 'com.bmuschko:gradle-nexus-plugin:2.3.1' 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | jcenter() 17 | google() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /circle-menu-simple-example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /circle-menu-simple-example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion '28.0.3' 6 | defaultConfig { 7 | applicationId "com.ramotion.circlemenu.example.simple" 8 | minSdkVersion 16 9 | targetSdkVersion 28 10 | versionCode 2 11 | versionName "1.1" 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(include: ['*.jar'], dir: 'libs') 24 | implementation project(':circle-menu') 25 | 26 | implementation 'androidx.appcompat:appcompat:1.0.2' 27 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 28 | 29 | testImplementation 'junit:junit:4.12' 30 | androidTestImplementation 'androidx.test:runner:1.1.1' 31 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 32 | } 33 | -------------------------------------------------------------------------------- /circle-menu-simple-example/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 /home/user/tools/android-sdk-linux/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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /circle-menu-simple-example/src/androidTest/java/com/ramotion/circlemenu/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.ramotion.circlemenu; 2 | 3 | import android.content.Context; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | 8 | import androidx.test.InstrumentationRegistry; 9 | import androidx.test.runner.AndroidJUnit4; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumentation test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() throws Exception { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("com.ramotion.circlemenu", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/java/com/ramotion/circlemenu/example/simple/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.ramotion.circlemenu.example.simple; 2 | 3 | import android.os.Bundle; 4 | import android.util.Log; 5 | 6 | import com.ramotion.circlemenu.CircleMenuView; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.appcompat.app.AppCompatActivity; 10 | 11 | 12 | public class MainActivity extends AppCompatActivity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_main); 18 | 19 | final CircleMenuView menu = findViewById(R.id.circle_menu); 20 | menu.setEventListener(new CircleMenuView.EventListener() { 21 | @Override 22 | public void onMenuOpenAnimationStart(@NonNull CircleMenuView view) { 23 | Log.d("D", "onMenuOpenAnimationStart"); 24 | } 25 | 26 | @Override 27 | public void onMenuOpenAnimationEnd(@NonNull CircleMenuView view) { 28 | Log.d("D", "onMenuOpenAnimationEnd"); 29 | } 30 | 31 | @Override 32 | public void onMenuCloseAnimationStart(@NonNull CircleMenuView view) { 33 | Log.d("D", "onMenuCloseAnimationStart"); 34 | } 35 | 36 | @Override 37 | public void onMenuCloseAnimationEnd(@NonNull CircleMenuView view) { 38 | Log.d("D", "onMenuCloseAnimationEnd"); 39 | } 40 | 41 | @Override 42 | public void onButtonClickAnimationStart(@NonNull CircleMenuView view, int index) { 43 | Log.d("D", "onButtonClickAnimationStart| index: " + index); 44 | } 45 | 46 | @Override 47 | public void onButtonClickAnimationEnd(@NonNull CircleMenuView view, int index) { 48 | Log.d("D", "onButtonClickAnimationEnd| index: " + index); 49 | } 50 | 51 | @Override 52 | public boolean onButtonLongClick(@NonNull CircleMenuView view, int index) { 53 | Log.d("D", "onButtonLongClick| index: " + index); 54 | return true; 55 | } 56 | 57 | @Override 58 | public void onButtonLongClickAnimationStart(@NonNull CircleMenuView view, int index) { 59 | Log.d("D", "onButtonLongClickAnimationStart| index: " + index); 60 | } 61 | 62 | @Override 63 | public void onButtonLongClickAnimationEnd(@NonNull CircleMenuView view, int index) { 64 | Log.d("D", "onButtonLongClickAnimationEnd| index: " + index); 65 | } 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-hdpi/ic_home_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-hdpi/ic_home_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-hdpi/ic_notifications_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-hdpi/ic_notifications_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-hdpi/ic_place_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-hdpi/ic_place_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-hdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-hdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-hdpi/ic_settings_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-hdpi/ic_settings_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-mdpi/ic_home_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-mdpi/ic_home_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-mdpi/ic_notifications_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-mdpi/ic_notifications_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-mdpi/ic_place_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-mdpi/ic_place_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-mdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-mdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-mdpi/ic_settings_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-mdpi/ic_settings_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xhdpi/ic_home_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xhdpi/ic_home_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xhdpi/ic_notifications_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xhdpi/ic_notifications_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xhdpi/ic_place_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xhdpi/ic_place_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xhdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xhdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xxhdpi/ic_home_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xxhdpi/ic_home_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xxhdpi/ic_notifications_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xxhdpi/ic_notifications_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xxhdpi/ic_place_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xxhdpi/ic_place_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xxxhdpi/ic_home_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xxxhdpi/ic_home_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xxxhdpi/ic_notifications_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xxxhdpi/ic_notifications_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xxxhdpi/ic_place_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xxxhdpi/ic_place_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu-simple-example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/values/buttons.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @drawable/ic_home_white_24dp 5 | @drawable/ic_search_white_24dp 6 | @drawable/ic_notifications_white_24dp 7 | @drawable/ic_settings_white_24dp 8 | @drawable/ic_place_white_24dp 9 | 10 | 11 | @android:color/holo_blue_light 12 | @android:color/holo_green_dark 13 | @android:color/holo_red_light 14 | @android:color/holo_purple 15 | @android:color/holo_orange_light 16 | 17 | -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | circle-menu-android 3 | 4 | -------------------------------------------------------------------------------- /circle-menu-simple-example/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /circle-menu-simple-example/src/test/java/com/ramotion/circlemenu/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.ramotion.circlemenu; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void additionIsCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /circle-menu/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /circle-menu/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'signing' 3 | apply plugin: 'com.bmuschko.nexus' 4 | 5 | group = 'com.ramotion.circlemenu' 6 | version = '0.3.2' 7 | 8 | android { 9 | compileSdkVersion 28 10 | buildToolsVersion '28.0.3' 11 | 12 | defaultConfig { 13 | minSdkVersion 16 14 | targetSdkVersion 28 15 | versionCode 6 16 | versionName version 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | 20 | } 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation fileTree(dir: 'libs', include: ['*.jar']) 31 | 32 | implementation 'com.google.android.material:material:1.1.0-alpha03' 33 | 34 | testImplementation 'junit:junit:4.12' 35 | androidTestImplementation 'androidx.test:runner:1.1.1' 36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 37 | } 38 | 39 | modifyPom { 40 | project { 41 | name 'Circle Menu for Android' 42 | description 'CircleMenu is a simple, elegant UI menu with a circular layout and material design animations. http://ramotion.com' 43 | url 'https://github.com/Ramotion/circle-menu-android' 44 | inceptionYear '2017' 45 | 46 | scm { 47 | url 'https://github.com/Ramotion/circle-menu-android' 48 | connection 'scm:git@github.com:Ramotion/circle-menu-android.git' 49 | developerConnection 'scm:git@github.com:Ramotion/circle-menu-android.git' 50 | } 51 | 52 | licenses { 53 | license { 54 | name 'The MIT License (MIT)' 55 | url 'https://opensource.org/licenses/mit-license.php' 56 | distribution 'repo' 57 | } 58 | } 59 | 60 | developers { 61 | developer { 62 | id 'dvg4000' 63 | name 'Dmitry Grishechkin' 64 | email 'dvgrishechkin@yandex.ru' 65 | } 66 | 67 | developer { 68 | id 'golovin47' 69 | name 'Ivan Golovin' 70 | email 'golovinivans@gmail.com' 71 | } 72 | } 73 | } 74 | } 75 | 76 | nexus { 77 | sign = true 78 | repositoryUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' 79 | snapshotRepositoryUrl = 'https://oss.sonatype.org/content/repositories/snapshots/' 80 | } -------------------------------------------------------------------------------- /circle-menu/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 /home/user/tools/android-sdk-linux/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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /circle-menu/src/androidTest/java/com/ramotion/circlemenu/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.ramotion.circlemenu; 2 | 3 | import android.content.Context; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | 8 | import androidx.test.InstrumentationRegistry; 9 | import androidx.test.runner.AndroidJUnit4; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumentation test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() throws Exception { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("com.ramotion.circlemenu.test", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /circle-menu/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /circle-menu/src/main/java/com/ramotion/circlemenu/CircleMenuView.java: -------------------------------------------------------------------------------- 1 | package com.ramotion.circlemenu; 2 | 3 | 4 | import android.animation.Animator; 5 | import android.animation.AnimatorListenerAdapter; 6 | import android.animation.AnimatorSet; 7 | import android.animation.Keyframe; 8 | import android.animation.ObjectAnimator; 9 | import android.animation.PropertyValuesHolder; 10 | import android.animation.ValueAnimator; 11 | import android.content.Context; 12 | import android.content.res.ColorStateList; 13 | import android.content.res.TypedArray; 14 | import android.graphics.Color; 15 | import android.graphics.Rect; 16 | import android.os.Build; 17 | import android.util.AttributeSet; 18 | import android.view.LayoutInflater; 19 | import android.view.View; 20 | import android.view.animation.OvershootInterpolator; 21 | import android.widget.FrameLayout; 22 | 23 | import com.google.android.material.floatingactionbutton.FloatingActionButton; 24 | 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | import androidx.annotation.AttrRes; 29 | import androidx.annotation.DrawableRes; 30 | import androidx.annotation.NonNull; 31 | import androidx.annotation.Nullable; 32 | import androidx.core.view.ViewCompat; 33 | 34 | 35 | /** 36 | * CircleMenuView 37 | */ 38 | public class CircleMenuView extends FrameLayout { 39 | 40 | private static final int DEFAULT_BUTTON_SIZE = 56; 41 | private static final float DEFAULT_DISTANCE = DEFAULT_BUTTON_SIZE * 1.5f; 42 | private static final float DEFAULT_RING_SCALE_RATIO = 1.3f; 43 | private static final float DEFAULT_CLOSE_ICON_ALPHA = 0.3f; 44 | 45 | private final List mButtons = new ArrayList<>(); 46 | private final Rect mButtonRect = new Rect(); 47 | 48 | private FloatingActionButton mMenuButton; 49 | private RingEffectView mRingView; 50 | 51 | private boolean mClosedState = true; 52 | private boolean mIsAnimating = false; 53 | 54 | private int mIconMenu; 55 | private int mIconClose; 56 | private int mDurationRing; 57 | private int mLongClickDurationRing; 58 | private int mDurationOpen; 59 | private int mDurationClose; 60 | private int mDesiredSize; 61 | private int mRingRadius; 62 | 63 | private float mDistance; 64 | 65 | private EventListener mListener; 66 | 67 | /** 68 | * CircleMenu event listener. 69 | */ 70 | public static class EventListener { 71 | /** 72 | * Invoked on menu button click, before animation start. 73 | * @param view current CircleMenuView instance. 74 | */ 75 | public void onMenuOpenAnimationStart(@NonNull CircleMenuView view) {} 76 | 77 | /** 78 | * Invoked on menu button click, after animation end. 79 | * @param view - current CircleMenuView instance. 80 | */ 81 | public void onMenuOpenAnimationEnd(@NonNull CircleMenuView view) {} 82 | 83 | /** 84 | * Invoked on close menu button click, before animation start. 85 | * @param view - current CircleMenuView instance. 86 | */ 87 | public void onMenuCloseAnimationStart(@NonNull CircleMenuView view) {} 88 | 89 | /** 90 | * Invoked on close menu button click, after animation end. 91 | * @param view - current CircleMenuView instance. 92 | */ 93 | public void onMenuCloseAnimationEnd(@NonNull CircleMenuView view) {} 94 | 95 | /** 96 | * Invoked on button click, before animation start. 97 | * @param view - current CircleMenuView instance. 98 | * @param buttonIndex - clicked button zero-based index. 99 | */ 100 | public void onButtonClickAnimationStart(@NonNull CircleMenuView view, int buttonIndex) {} 101 | 102 | /** 103 | * Invoked on button click, after animation end. 104 | * @param view - current CircleMenuView instance. 105 | * @param buttonIndex - clicked button zero-based index. 106 | */ 107 | public void onButtonClickAnimationEnd(@NonNull CircleMenuView view, int buttonIndex) {} 108 | 109 | /** 110 | * Invoked on button long click. Invokes {@see onButtonLongClickAnimationStart} and {@see onButtonLongClickAnimationEnd} 111 | * if returns true. 112 | * @param view current CircleMenuView instance. 113 | * @param buttonIndex clicked button zero-based index. 114 | * @return true if the callback consumed the long click, false otherwise. 115 | */ 116 | public boolean onButtonLongClick(@NonNull CircleMenuView view, int buttonIndex) { return false; } 117 | 118 | /** 119 | * Invoked on button long click, before animation start. 120 | * @param view - current CircleMenuView instance. 121 | * @param buttonIndex - clicked button zero-based index. 122 | */ 123 | public void onButtonLongClickAnimationStart(@NonNull CircleMenuView view, int buttonIndex) {} 124 | 125 | /** 126 | * Invoked on button long click, after animation end. 127 | * @param view - current CircleMenuView instance. 128 | * @param buttonIndex - clicked button zero-based index. 129 | */ 130 | public void onButtonLongClickAnimationEnd(@NonNull CircleMenuView view, int buttonIndex) {} 131 | } 132 | 133 | private class OnButtonClickListener implements View.OnClickListener { 134 | @Override 135 | public void onClick(final View view) { 136 | if (mIsAnimating) { 137 | return; 138 | } 139 | 140 | final Animator click = getButtonClickAnimation((FloatingActionButton)view); 141 | click.setDuration(mDurationRing); 142 | click.addListener(new AnimatorListenerAdapter() { 143 | @Override 144 | public void onAnimationStart(Animator animation) { 145 | if (mListener != null) { 146 | mListener.onButtonClickAnimationStart(CircleMenuView.this, mButtons.indexOf(view)); 147 | } 148 | } 149 | @Override 150 | public void onAnimationEnd(Animator animation) { 151 | mClosedState = true; 152 | if (mListener != null) { 153 | mListener.onButtonClickAnimationEnd(CircleMenuView.this, mButtons.indexOf(view)); 154 | } 155 | } 156 | }); 157 | click.start(); 158 | } 159 | } 160 | 161 | private class OnButtonLongClickListener implements View.OnLongClickListener { 162 | @Override 163 | public boolean onLongClick(final View view) { 164 | if (mListener == null) { 165 | return false; 166 | } 167 | 168 | final boolean result = mListener.onButtonLongClick(CircleMenuView.this, mButtons.indexOf(view)); 169 | if (result && !mIsAnimating) { 170 | final Animator click = getButtonClickAnimation((FloatingActionButton)view); 171 | click.setDuration(mLongClickDurationRing); 172 | click.addListener(new AnimatorListenerAdapter() { 173 | @Override 174 | public void onAnimationStart(Animator animation) { 175 | mListener.onButtonLongClickAnimationStart(CircleMenuView.this, mButtons.indexOf(view)); 176 | } 177 | @Override 178 | public void onAnimationEnd(Animator animation) { 179 | mClosedState = true; 180 | mListener.onButtonLongClickAnimationEnd(CircleMenuView.this, mButtons.indexOf(view)); 181 | } 182 | }); 183 | click.start(); 184 | } 185 | 186 | return result; 187 | } 188 | } 189 | 190 | public CircleMenuView(@NonNull Context context, @Nullable AttributeSet attrs) { 191 | this(context, attrs, 0); 192 | } 193 | 194 | public CircleMenuView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { 195 | super(context, attrs, defStyleAttr); 196 | 197 | if (attrs == null) { 198 | throw new IllegalArgumentException("No buttons icons or colors set"); 199 | } 200 | 201 | final int menuButtonColor; 202 | final List icons; 203 | final List colors; 204 | 205 | final TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CircleMenuView, 0, 0); 206 | try { 207 | final int iconArrayId = a.getResourceId(R.styleable.CircleMenuView_button_icons, 0); 208 | final int colorArrayId = a.getResourceId(R.styleable.CircleMenuView_button_colors, 0); 209 | 210 | final TypedArray iconsIds = getResources().obtainTypedArray(iconArrayId); 211 | try { 212 | final int[] colorsIds = getResources().getIntArray(colorArrayId); 213 | final int buttonsCount = Math.min(iconsIds.length(), colorsIds.length); 214 | 215 | icons = new ArrayList<>(buttonsCount); 216 | colors = new ArrayList<>(buttonsCount); 217 | 218 | for (int i = 0; i < buttonsCount; i++) { 219 | icons.add(iconsIds.getResourceId(i, -1)); 220 | colors.add(colorsIds[i]); 221 | } 222 | } finally { 223 | iconsIds.recycle(); 224 | } 225 | 226 | mIconMenu = a.getResourceId(R.styleable.CircleMenuView_icon_menu, R.drawable.ic_menu_black_24dp); 227 | mIconClose = a.getResourceId(R.styleable.CircleMenuView_icon_close, R.drawable.ic_close_black_24dp); 228 | 229 | mDurationRing = a.getInteger(R.styleable.CircleMenuView_duration_ring, getResources().getInteger(android.R.integer.config_mediumAnimTime)); 230 | mLongClickDurationRing = a.getInteger(R.styleable.CircleMenuView_long_click_duration_ring, getResources().getInteger(android.R.integer.config_longAnimTime)); 231 | mDurationOpen = a.getInteger(R.styleable.CircleMenuView_duration_open, getResources().getInteger(android.R.integer.config_mediumAnimTime)); 232 | mDurationClose = a.getInteger(R.styleable.CircleMenuView_duration_close, getResources().getInteger(android.R.integer.config_mediumAnimTime)); 233 | 234 | final float density = context.getResources().getDisplayMetrics().density; 235 | final float defaultDistance = DEFAULT_DISTANCE * density; 236 | mDistance = a.getDimension(R.styleable.CircleMenuView_distance, defaultDistance); 237 | 238 | menuButtonColor = a.getColor(R.styleable.CircleMenuView_icon_color, Color.WHITE); 239 | } finally { 240 | a.recycle(); 241 | } 242 | 243 | initLayout(context); 244 | initMenu(menuButtonColor); 245 | initButtons(context, icons, colors); 246 | } 247 | 248 | /** 249 | * Constructor for creation CircleMenuView in code, not in xml-layout. 250 | * @param context current context, will be used to access resources. 251 | * @param icons buttons icons resource ids array. Items must be @DrawableRes. 252 | * @param colors buttons colors resource ids array. Items must be @DrawableRes. 253 | */ 254 | public CircleMenuView(@NonNull Context context, @NonNull List icons, @NonNull List colors) { 255 | super(context); 256 | 257 | final float density = context.getResources().getDisplayMetrics().density; 258 | final float defaultDistance = DEFAULT_DISTANCE * density; 259 | 260 | mIconMenu = R.drawable.ic_menu_black_24dp; 261 | mIconClose = R.drawable.ic_close_black_24dp; 262 | 263 | mDurationRing = getResources().getInteger(android.R.integer.config_mediumAnimTime); 264 | mLongClickDurationRing = getResources().getInteger(android.R.integer.config_longAnimTime); 265 | mDurationOpen = getResources().getInteger(android.R.integer.config_mediumAnimTime); 266 | mDurationClose = getResources().getInteger(android.R.integer.config_mediumAnimTime); 267 | 268 | mDistance = defaultDistance; 269 | 270 | initLayout(context); 271 | initMenu(Color.WHITE); 272 | initButtons(context, icons, colors); 273 | } 274 | 275 | @Override 276 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 277 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 278 | 279 | final int w = resolveSizeAndState(mDesiredSize, widthMeasureSpec, 0); 280 | final int h = resolveSizeAndState(mDesiredSize, heightMeasureSpec, 0); 281 | 282 | setMeasuredDimension(w, h); 283 | } 284 | 285 | @Override 286 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 287 | super.onLayout(changed, left, top, right, bottom); 288 | 289 | if (!changed && mIsAnimating) { 290 | return; 291 | } 292 | 293 | mMenuButton.getContentRect(mButtonRect); 294 | 295 | mRingView.setStrokeWidth(mButtonRect.width()); 296 | mRingView.setRadius(mRingRadius); 297 | 298 | final LayoutParams lp = (LayoutParams) mRingView.getLayoutParams(); 299 | lp.width = right - left; 300 | lp.height = bottom - top; 301 | } 302 | 303 | private void initLayout(@NonNull Context context) { 304 | LayoutInflater.from(context).inflate(R.layout.circle_menu, this, true); 305 | 306 | setWillNotDraw(true); 307 | setClipChildren(false); 308 | setClipToPadding(false); 309 | 310 | final float density = context.getResources().getDisplayMetrics().density; 311 | final float buttonSize = DEFAULT_BUTTON_SIZE * density; 312 | 313 | mRingRadius = (int) (buttonSize + (mDistance - buttonSize / 2)); 314 | mDesiredSize = (int) (mRingRadius * 2 * DEFAULT_RING_SCALE_RATIO); 315 | 316 | mRingView = findViewById(R.id.ring_view); 317 | } 318 | 319 | private void initMenu(int menuButtonColor) { 320 | final AnimatorListenerAdapter animListener = new AnimatorListenerAdapter() { 321 | @Override 322 | public void onAnimationStart(Animator animation) { 323 | if (mListener != null) { 324 | if (mClosedState) { 325 | mListener.onMenuOpenAnimationStart(CircleMenuView.this); 326 | } else { 327 | mListener.onMenuCloseAnimationStart(CircleMenuView.this); 328 | } 329 | } 330 | } 331 | @Override 332 | public void onAnimationEnd(Animator animation) { 333 | if (mListener != null) { 334 | if (mClosedState) { 335 | mListener.onMenuOpenAnimationEnd(CircleMenuView.this); 336 | } else { 337 | mListener.onMenuCloseAnimationEnd(CircleMenuView.this); 338 | } 339 | } 340 | 341 | mClosedState = !mClosedState; 342 | } 343 | }; 344 | 345 | mMenuButton = findViewById(R.id.circle_menu_main_button); 346 | mMenuButton.setImageResource(mIconMenu); 347 | mMenuButton.setBackgroundTintList(ColorStateList.valueOf(menuButtonColor)); 348 | mMenuButton.setOnClickListener(new OnClickListener() { 349 | @Override 350 | public void onClick(View view) { 351 | if (mIsAnimating) { 352 | return; 353 | } 354 | 355 | final Animator animation = mClosedState ? getOpenMenuAnimation() : getCloseMenuAnimation(); 356 | animation.setDuration(mClosedState ? mDurationClose : mDurationOpen); 357 | animation.addListener(animListener); 358 | animation.start(); 359 | } 360 | }); 361 | } 362 | 363 | private void initButtons(@NonNull Context context, @NonNull List icons, @NonNull List colors) { 364 | final int buttonsCount = Math.min(icons.size(), colors.size()); 365 | for (int i = 0; i < buttonsCount; i++) { 366 | final FloatingActionButton button = new FloatingActionButton(context); 367 | button.setImageResource(icons.get(i)); 368 | button.setBackgroundTintList(ColorStateList.valueOf(colors.get(i))); 369 | button.setClickable(true); 370 | button.setOnClickListener(new OnButtonClickListener()); 371 | button.setOnLongClickListener(new OnButtonLongClickListener()); 372 | button.setScaleX(0); 373 | button.setScaleY(0); 374 | button.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); 375 | 376 | addView(button); 377 | mButtons.add(button); 378 | } 379 | } 380 | 381 | private void offsetAndScaleButtons(float centerX, float centerY, float angleStep, float offset, float scale) { 382 | for (int i = 0, cnt = mButtons.size(); i < cnt; i++) { 383 | final float angle = angleStep * i - 90; 384 | final float x = (float) Math.cos(Math.toRadians(angle)) * offset; 385 | final float y = (float) Math.sin(Math.toRadians(angle)) * offset; 386 | 387 | final View button = mButtons.get(i); 388 | button.setX(centerX + x); 389 | button.setY(centerY + y); 390 | button.setScaleX(1.0f * scale); 391 | button.setScaleY(1.0f * scale); 392 | } 393 | } 394 | 395 | private Animator getButtonClickAnimation(final @NonNull FloatingActionButton button) { 396 | final int buttonNumber = mButtons.indexOf(button) + 1; 397 | final float stepAngle = 360f / mButtons.size(); 398 | final float rOStartAngle = (270 - stepAngle + stepAngle * buttonNumber); 399 | final float rStartAngle = rOStartAngle > 360 ? rOStartAngle % 360 : rOStartAngle; 400 | 401 | final float x = (float) Math.cos(Math.toRadians(rStartAngle)) * mDistance; 402 | final float y = (float) Math.sin(Math.toRadians(rStartAngle)) * mDistance; 403 | 404 | final float pivotX = button.getPivotX(); 405 | final float pivotY = button.getPivotY(); 406 | button.setPivotX(pivotX - x); 407 | button.setPivotY(pivotY - y); 408 | 409 | final ObjectAnimator rotateButton = ObjectAnimator.ofFloat(button, "rotation", 0f, 360f); 410 | rotateButton.addListener(new AnimatorListenerAdapter() { 411 | @Override 412 | public void onAnimationEnd(Animator animation) { 413 | button.setPivotX(pivotX); 414 | button.setPivotY(pivotY); 415 | } 416 | }); 417 | 418 | final float elevation = mMenuButton.getCompatElevation(); 419 | 420 | mRingView.setVisibility(View.INVISIBLE); 421 | mRingView.setStartAngle(rStartAngle); 422 | 423 | final ColorStateList csl = button.getBackgroundTintList(); 424 | if (csl != null) { 425 | mRingView.setStrokeColor(csl.getDefaultColor()); 426 | } 427 | 428 | final ObjectAnimator ring = ObjectAnimator.ofFloat(mRingView, "angle", 360); 429 | final ObjectAnimator scaleX = ObjectAnimator.ofFloat(mRingView, "scaleX", 1f, DEFAULT_RING_SCALE_RATIO); 430 | final ObjectAnimator scaleY = ObjectAnimator.ofFloat(mRingView, "scaleY", 1f, DEFAULT_RING_SCALE_RATIO); 431 | final ObjectAnimator visible = ObjectAnimator.ofFloat(mRingView, "alpha", 1f, 0f); 432 | 433 | final AnimatorSet lastSet = new AnimatorSet(); 434 | lastSet.playTogether(scaleX, scaleY, visible, getCloseMenuAnimation()); 435 | 436 | final AnimatorSet firstSet = new AnimatorSet(); 437 | firstSet.playTogether(rotateButton, ring); 438 | 439 | final AnimatorSet result = new AnimatorSet(); 440 | result.play(firstSet).before(lastSet); 441 | result.addListener(new AnimatorListenerAdapter() { 442 | @Override 443 | public void onAnimationStart(Animator animation) { 444 | mIsAnimating = true; 445 | 446 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 447 | bringChildToFront(mRingView); 448 | bringChildToFront(button); 449 | } else { 450 | button.setCompatElevation(elevation + 1); 451 | ViewCompat.setZ(mRingView, elevation + 1); 452 | 453 | for (View b : mButtons) { 454 | if (b != button) { 455 | ((FloatingActionButton) b).setCompatElevation(0); 456 | } 457 | } 458 | } 459 | 460 | mRingView.setScaleX(1f); 461 | mRingView.setScaleY(1f); 462 | mRingView.setVisibility(View.VISIBLE); 463 | } 464 | @Override 465 | public void onAnimationEnd(Animator animation) { 466 | mIsAnimating = false; 467 | 468 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 469 | for (View b : mButtons) { 470 | ((FloatingActionButton) b).setCompatElevation(elevation); 471 | } 472 | 473 | ViewCompat.setZ(mRingView, elevation); 474 | } 475 | } 476 | }); 477 | 478 | return result; 479 | } 480 | 481 | private Animator getOpenMenuAnimation() { 482 | final ObjectAnimator alphaAnimation = ObjectAnimator.ofFloat(mMenuButton, "alpha", DEFAULT_CLOSE_ICON_ALPHA); 483 | 484 | final Keyframe kf0 = Keyframe.ofFloat(0f, 0f); 485 | final Keyframe kf1 = Keyframe.ofFloat(0.5f, 60f); 486 | final Keyframe kf2 = Keyframe.ofFloat(1f, 0f); 487 | final PropertyValuesHolder pvhRotation = PropertyValuesHolder.ofKeyframe("rotation", kf0, kf1, kf2); 488 | final ObjectAnimator rotateAnimation = ObjectAnimator.ofPropertyValuesHolder(mMenuButton, pvhRotation); 489 | rotateAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 490 | private boolean iconChanged = false; 491 | @Override 492 | public void onAnimationUpdate(ValueAnimator valueAnimator) { 493 | final float fraction = valueAnimator.getAnimatedFraction(); 494 | if (fraction >= 0.5f && !iconChanged) { 495 | iconChanged = true; 496 | mMenuButton.setImageResource(mIconClose); 497 | } 498 | } 499 | }); 500 | 501 | final float centerX = mMenuButton.getX(); 502 | final float centerY = mMenuButton.getY(); 503 | 504 | final int buttonsCount = mButtons.size(); 505 | final float angleStep = 360f / buttonsCount; 506 | 507 | final ValueAnimator buttonsAppear = ValueAnimator.ofFloat(0f, mDistance); 508 | buttonsAppear.setInterpolator(new OvershootInterpolator()); 509 | buttonsAppear.addListener(new AnimatorListenerAdapter() { 510 | @Override 511 | public void onAnimationStart(Animator animation) { 512 | for (View view: mButtons) { 513 | view.setVisibility(View.VISIBLE); 514 | } 515 | } 516 | }); 517 | buttonsAppear.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 518 | @Override 519 | public void onAnimationUpdate(ValueAnimator valueAnimator) { 520 | final float fraction = valueAnimator.getAnimatedFraction(); 521 | final float value = (float)valueAnimator.getAnimatedValue(); 522 | offsetAndScaleButtons(centerX, centerY, angleStep, value, fraction); 523 | } 524 | }); 525 | 526 | final AnimatorSet result = new AnimatorSet(); 527 | result.playTogether(alphaAnimation, rotateAnimation, buttonsAppear); 528 | result.addListener(new AnimatorListenerAdapter() { 529 | @Override 530 | public void onAnimationStart(Animator animation) { 531 | mIsAnimating = true; 532 | } 533 | @Override 534 | public void onAnimationEnd(Animator animation) { 535 | mIsAnimating = false; 536 | } 537 | }); 538 | 539 | return result; 540 | } 541 | 542 | private Animator getCloseMenuAnimation() { 543 | final ObjectAnimator scaleX1 = ObjectAnimator.ofFloat(mMenuButton, "scaleX", 0f); 544 | final ObjectAnimator scaleY1 = ObjectAnimator.ofFloat(mMenuButton, "scaleY", 0f); 545 | final ObjectAnimator alpha1 = ObjectAnimator.ofFloat(mMenuButton, "alpha", 0f); 546 | final AnimatorSet set1 = new AnimatorSet(); 547 | set1.playTogether(scaleX1, scaleY1, alpha1); 548 | set1.addListener(new AnimatorListenerAdapter() { 549 | @Override 550 | public void onAnimationStart(Animator animation) { 551 | for (View view: mButtons) { 552 | view.setVisibility(View.INVISIBLE); 553 | } 554 | } 555 | @Override 556 | public void onAnimationEnd(Animator animation) { 557 | mMenuButton.setRotation(60f); 558 | mMenuButton.setImageResource(mIconMenu); 559 | } 560 | }); 561 | 562 | final ObjectAnimator angle = ObjectAnimator.ofFloat(mMenuButton, "rotation", 0); 563 | final ObjectAnimator alpha2 = ObjectAnimator.ofFloat(mMenuButton, "alpha", 1f); 564 | final ObjectAnimator scaleX2 = ObjectAnimator.ofFloat(mMenuButton, "scaleX", 1f); 565 | final ObjectAnimator scaleY2 = ObjectAnimator.ofFloat(mMenuButton, "scaleY", 1f); 566 | final AnimatorSet set2 = new AnimatorSet(); 567 | set2.setInterpolator(new OvershootInterpolator()); 568 | set2.playTogether(angle, alpha2, scaleX2, scaleY2); 569 | 570 | final AnimatorSet result = new AnimatorSet(); 571 | result.play(set1).before(set2); 572 | result.addListener(new AnimatorListenerAdapter() { 573 | @Override 574 | public void onAnimationStart(Animator animation) { 575 | mIsAnimating = true; 576 | } 577 | @Override 578 | public void onAnimationEnd(Animator animation) { 579 | mIsAnimating = false; 580 | } 581 | }); 582 | return result; 583 | } 584 | 585 | public void setIconMenu(@DrawableRes int iconId) { 586 | mIconMenu = iconId; 587 | } 588 | 589 | @DrawableRes 590 | public int getIconMenu() { 591 | return mIconMenu; 592 | } 593 | 594 | public void setIconClose(@DrawableRes int iconId) { 595 | mIconClose = iconId; 596 | } 597 | 598 | @DrawableRes 599 | public int getIconClose() { 600 | return mIconClose; 601 | } 602 | 603 | /** 604 | * See {@link R.styleable#CircleMenuView_duration_close} 605 | * @param duration close animation duration in milliseconds. 606 | */ 607 | public void setDurationClose(int duration) { 608 | mDurationClose = duration; 609 | } 610 | 611 | /** 612 | * See {@link R.styleable#CircleMenuView_duration_close} 613 | * @return current close animation duration. 614 | */ 615 | public int getDurationClose() { 616 | return mDurationClose; 617 | } 618 | 619 | /** 620 | * See {@link R.styleable#CircleMenuView_duration_open} 621 | * @param duration open animation duration in milliseconds. 622 | */ 623 | public void setDurationOpen(int duration) { 624 | mDurationOpen = duration; 625 | } 626 | 627 | /** 628 | * See {@link R.styleable#CircleMenuView_duration_open} 629 | * @return current open animation duration. 630 | */ 631 | public int getDurationOpen() { 632 | return mDurationOpen; 633 | } 634 | 635 | /** 636 | * See {@link R.styleable#CircleMenuView_duration_ring} 637 | * @param duration ring animation duration in milliseconds. 638 | */ 639 | public void setDurationRing(int duration) { 640 | mDurationRing = duration; 641 | } 642 | 643 | /** 644 | * See {@link R.styleable#CircleMenuView_duration_ring} 645 | * @return current ring animation duration. 646 | */ 647 | public int getDurationRing() { 648 | return mDurationRing; 649 | } 650 | 651 | /** 652 | * See {@link R.styleable#CircleMenuView_long_click_duration_ring} 653 | * @return current long click ring animation duration. 654 | */ 655 | public int getLongClickDurationRing() { 656 | return mLongClickDurationRing; 657 | } 658 | 659 | /** 660 | * See {@link R.styleable#CircleMenuView_long_click_duration_ring} 661 | * @param duration long click ring animation duration in milliseconds. 662 | */ 663 | public void setLongClickDurationRing(int duration) { 664 | mLongClickDurationRing = duration; 665 | } 666 | 667 | /** 668 | * See {@link R.styleable#CircleMenuView_distance} 669 | * @param distance in pixels. 670 | */ 671 | public void setDistance(float distance) { 672 | mDistance = distance; 673 | invalidate(); 674 | } 675 | 676 | /** 677 | * See {@link R.styleable#CircleMenuView_distance} 678 | * @return current distance in pixels. 679 | */ 680 | public float getDistance() { 681 | return mDistance; 682 | } 683 | 684 | /** 685 | * See {@link CircleMenuView.EventListener } 686 | * @param listener new event listener or null. 687 | */ 688 | public void setEventListener(@Nullable EventListener listener) { 689 | mListener = listener; 690 | } 691 | 692 | /** 693 | * See {@link CircleMenuView.EventListener } 694 | * @return current event listener or null. 695 | */ 696 | public EventListener getEventListener() { 697 | return mListener; 698 | } 699 | 700 | private void openOrClose(boolean open, boolean animate) { 701 | if (mIsAnimating) { 702 | return; 703 | } 704 | 705 | if (open && !mClosedState) { 706 | return; 707 | } 708 | 709 | if (!open && mClosedState) { 710 | return; 711 | } 712 | 713 | if (animate) { 714 | mMenuButton.performClick(); 715 | } else { 716 | mClosedState = !open; 717 | 718 | final float centerX = mMenuButton.getX(); 719 | final float centerY = mMenuButton.getY(); 720 | 721 | final int buttonsCount = mButtons.size(); 722 | final float angleStep = 360f / buttonsCount; 723 | 724 | final float offset = open ? mDistance : 0f; 725 | final float scale = open ? 1f : 0f; 726 | 727 | mMenuButton.setImageResource(open ? mIconClose : mIconMenu); 728 | mMenuButton.setAlpha(open ? DEFAULT_CLOSE_ICON_ALPHA : 1f); 729 | 730 | final int visibility = open ? View.VISIBLE : View.INVISIBLE; 731 | for (View view: mButtons) { 732 | view.setVisibility(visibility); 733 | } 734 | 735 | offsetAndScaleButtons(centerX, centerY, angleStep, offset, scale); 736 | } 737 | } 738 | 739 | /** 740 | * Open menu programmatically 741 | * @param animate open with animation or not 742 | */ 743 | public void open(boolean animate) { 744 | openOrClose(true, animate); 745 | } 746 | 747 | /** 748 | * Close menu programmatically 749 | * @param animate close with animation or not 750 | */ 751 | public void close(boolean animate) { 752 | openOrClose(false, animate); 753 | } 754 | 755 | } 756 | -------------------------------------------------------------------------------- /circle-menu/src/main/java/com/ramotion/circlemenu/RingEffectView.java: -------------------------------------------------------------------------------- 1 | package com.ramotion.circlemenu; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.graphics.Path; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | 10 | import androidx.annotation.FloatRange; 11 | import androidx.annotation.Nullable; 12 | 13 | 14 | public class RingEffectView extends View { 15 | 16 | private static final int STEP_DEGREE = 5; 17 | 18 | private final Paint mPaint; 19 | private final Path mPath = new Path(); 20 | 21 | private float mAngle; 22 | private float mStartAngle; 23 | private int mRadius; 24 | 25 | public RingEffectView(Context context) { 26 | this(context, null); 27 | } 28 | 29 | public RingEffectView(Context context, @Nullable AttributeSet attrs) { 30 | super(context, attrs); 31 | 32 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 33 | mPaint.setStyle(Paint.Style.STROKE); 34 | mPaint.setStrokeCap(Paint.Cap.ROUND); 35 | } 36 | 37 | @Override 38 | protected void onDraw(Canvas canvas) { 39 | super.onDraw(canvas); 40 | 41 | if (!mPath.isEmpty()) { 42 | canvas.save(); 43 | canvas.translate(getWidth() / 2, getHeight() / 2); 44 | canvas.drawPath(mPath, mPaint); 45 | canvas.restore(); 46 | } 47 | } 48 | 49 | @Override 50 | public void setAlpha(@FloatRange(from = 0.0, to = 1.0) float alpha) { 51 | mPaint.setAlpha((int)(255 * alpha)); 52 | invalidate(); 53 | } 54 | 55 | @Override 56 | public float getAlpha() { 57 | return mPaint.getAlpha() / 255; 58 | } 59 | 60 | public float getAngle() { 61 | return mAngle; 62 | } 63 | 64 | public void setAngle(@FloatRange(from = 0.0, to = 360.0) float angle) { 65 | final float diff = angle - mAngle; 66 | final int stepCount = (int) (diff / STEP_DEGREE); 67 | final float stepMod = diff % STEP_DEGREE; 68 | 69 | final float sw = mPaint.getStrokeWidth() * 0.5f; 70 | final float radius = mRadius - sw; 71 | 72 | for (int i = 1; i <= stepCount; i++ ) { 73 | final float stepAngel = mStartAngle + mAngle + STEP_DEGREE * i; 74 | final float x = (float) Math.cos(Math.toRadians(stepAngel)) * radius; 75 | final float y = (float) Math.sin(Math.toRadians(stepAngel)) * radius; 76 | mPath.lineTo(x, y); 77 | } 78 | 79 | final float stepAngel = mStartAngle + mAngle + STEP_DEGREE * stepCount + stepMod; 80 | final float x = (float) Math.cos(Math.toRadians(stepAngel)) * radius; 81 | final float y = (float) Math.sin(Math.toRadians(stepAngel)) * radius; 82 | mPath.lineTo(x, y); 83 | 84 | mAngle = angle; 85 | 86 | invalidate(); 87 | } 88 | 89 | public float getStartAngle() { 90 | return mStartAngle; 91 | } 92 | 93 | public void setStartAngle(@FloatRange(from = 0.0, to = 360.0) float startAngle) { 94 | mStartAngle = startAngle; 95 | mAngle = 0; 96 | 97 | final float sw = mPaint.getStrokeWidth() * 0.5f; 98 | final float radius = mRadius - sw; 99 | 100 | mPath.reset(); 101 | final float x = (float) Math.cos(Math.toRadians(startAngle)) * radius; 102 | final float y = (float) Math.sin(Math.toRadians(startAngle)) * radius; 103 | mPath.moveTo(x, y); 104 | } 105 | 106 | public void setStrokeColor(int color) { 107 | mPaint.setColor(color); 108 | } 109 | 110 | public void setStrokeWidth(int width) { 111 | mPaint.setStrokeWidth(width); 112 | } 113 | 114 | public void setRadius(int radius) { 115 | mRadius = radius; 116 | } 117 | 118 | public int getRadius() { 119 | return mRadius; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /circle-menu/src/main/res/drawable-hdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu/src/main/res/drawable-hdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /circle-menu/src/main/res/drawable-hdpi/ic_menu_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu/src/main/res/drawable-hdpi/ic_menu_black_24dp.png -------------------------------------------------------------------------------- /circle-menu/src/main/res/drawable-mdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu/src/main/res/drawable-mdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /circle-menu/src/main/res/drawable-mdpi/ic_menu_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu/src/main/res/drawable-mdpi/ic_menu_black_24dp.png -------------------------------------------------------------------------------- /circle-menu/src/main/res/drawable-xhdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu/src/main/res/drawable-xhdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /circle-menu/src/main/res/drawable-xhdpi/ic_menu_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu/src/main/res/drawable-xhdpi/ic_menu_black_24dp.png -------------------------------------------------------------------------------- /circle-menu/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /circle-menu/src/main/res/drawable-xxhdpi/ic_menu_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu/src/main/res/drawable-xxhdpi/ic_menu_black_24dp.png -------------------------------------------------------------------------------- /circle-menu/src/main/res/drawable-xxxhdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu/src/main/res/drawable-xxxhdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /circle-menu/src/main/res/drawable-xxxhdpi/ic_menu_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/circle-menu/src/main/res/drawable-xxxhdpi/ic_menu_black_24dp.png -------------------------------------------------------------------------------- /circle-menu/src/main/res/layout/circle_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 21 | 22 | -------------------------------------------------------------------------------- /circle-menu/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /circle-menu/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | circlemenu 3 | 4 | -------------------------------------------------------------------------------- /circle-menu/src/test/java/com/ramotion/circlemenu/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.ramotion.circlemenu; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /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 | android.useAndroidX=true 10 | android.enableJetifier=true 11 | # Specifies the JVM arguments used for the daemon process. 12 | # The setting is particularly useful for tweaking memory settings. 13 | org.gradle.jvmargs=-Xmx1536m 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 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Feb 11 14:44:36 MSK 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/header.png -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/circle-menu-android/ada9687c73827af54713614dbbf8c2a5bac79c68/preview.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':circle-menu-simple-example', ':circle-menu' 2 | --------------------------------------------------------------------------------