├── .gitignore ├── .idea ├── .name ├── checkstyle-idea.xml ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ym │ │ └── floatmenu │ │ └── path │ │ └── demo │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── ym │ │ │ └── floatmenu │ │ │ └── path │ │ │ └── demo │ │ │ ├── FlHandler.java │ │ │ ├── FloatLayout.java │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable │ │ ├── composer_camera.png │ │ ├── composer_music.png │ │ ├── composer_place.png │ │ ├── composer_sleep.png │ │ ├── composer_thought.png │ │ ├── composer_with.png │ │ └── ic_launcher.png │ │ ├── layout │ │ ├── activity_main.xml │ │ └── main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── ym │ └── floatmenu │ └── path │ └── demo │ └── ExampleUnitTest.java ├── build.gradle ├── floatpathmenu ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ym │ │ └── floatmenu │ │ └── path │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── ym │ │ │ └── floatmenu │ │ │ └── path │ │ │ ├── ArcPathMenu.java │ │ │ ├── PathMenu.java │ │ │ ├── PathMenuActivity.java │ │ │ ├── PathMenuLayout.java │ │ │ ├── PathMenuService.java │ │ │ └── RotateAndTranslateAnimation.java │ └── res │ │ ├── drawable-hdpi │ │ ├── composer_button.png │ │ ├── composer_camera.png │ │ ├── composer_icn_plus.png │ │ ├── composer_music.png │ │ ├── composer_place.png │ │ ├── composer_sleep.png │ │ ├── composer_sun.png │ │ ├── composer_thought.png │ │ ├── composer_with.png │ │ ├── friends_delete.png │ │ └── ic_launcher.png │ │ ├── drawable-mdpi │ │ └── ic_launcher.png │ │ ├── drawable-xhdpi │ │ └── ic_launcher.png │ │ ├── drawable-xxhdpi │ │ └── ic_launcher.png │ │ ├── drawable │ │ └── button.xml │ │ ├── layout │ │ ├── activity_float.xml │ │ └── float_menu.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── ym │ └── floatmenu │ └── path │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pic ├── QQ图片20160918183808.jpg ├── QQ图片20160919111057.jpg └── QQ图片20160919111211.jpg ├── projectFilesBackup └── .idea │ └── workspace.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | FloatPathMenuSample -------------------------------------------------------------------------------- /.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.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 | 47 | 48 | 49 | 50 | 1.7 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FloatPathMenuSample 2 | modified from daCapricorn/ArcMenu[ https://github.com/daCapricorn/ArcMenu](https://github.com/daCapricorn/ArcMenu) 3 | 4 | 5 | demo: 6 | 7 | 8 | 9 | 半径计算公式 : 10 | 半径==( 间距+子菜单大小) /2 x sin(夹角/2) 11 | 12 | 13 | 14 | 原理图: 15 | 16 | 17 | 18 | 19 | 原作者代码有一处bug,悬浮球居中时,子菜单数有两个重叠,原因是计算子菜单夹角时没考虑360度的情况,核心代码: 20 | 21 | final float perDegrees = Math.abs(mToDegrees - mFromDegrees) == 360 ? 22 | (mToDegrees - mFromDegrees) / (childCount) 23 | : (mToDegrees - mFromDegrees) / (childCount - 1); 24 | 25 | 26 | 需求来自 [linh47](https://github.com/linh47/PathMenu)的提问,在 [https://github.com/linh47/PathMenu](https://github.com/linh47/PathMenu)基础上增加悬浮可拖动功能,对应区域自动切换扇形区域,目前还有点小问题 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | - modified form [daCapricorn`s ArcMenu](https://github.com/daCapricorn/ArcMenu) 36 | - [https://github.com/crosg/FloatMenuSample](https://github.com/crosg/FloatMenuSample) 37 | 38 | - Contributors: 39 | [yiming](https://github.com/fanOfDemo) , [linh47](https://github.com/linh47/PathMenu) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion '24' 6 | 7 | defaultConfig { 8 | applicationId "com.ym.floatmenu.path.demo" 9 | minSdkVersion 9 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.3.0' 26 | compile project(':floatpathmenu') 27 | } 28 | -------------------------------------------------------------------------------- /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 D:\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/androidTest/java/com/ym/floatmenu/path/demo/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path.demo; 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 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/ym/floatmenu/path/demo/FlHandler.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path.demo; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | 7 | /** 8 | * 项目名称:androidUITraFloatLayout 9 | * 类描述: 10 | * 创建人:wengyiming 11 | * 创建时间:2016/9/14 17:13 12 | * 修改人:wengyiming 13 | * 修改时间:2016/9/14 17:13 14 | * 修改备注: 15 | */ 16 | public interface FlHandler { 17 | 18 | View onCreateView(LayoutInflater inflater, ViewGroup viewGroup); 19 | 20 | void onViewCreated(FloatLayout parent); 21 | 22 | void onFloatMoving(boolean isMoving, float deltX, float deltY); 23 | 24 | void onMoveCompletedLocation(boolean isRight); 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/ym/floatmenu/path/demo/FloatLayout.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path.demo; 2 | 3 | import android.content.Context; 4 | import android.graphics.PixelFormat; 5 | import android.os.Build; 6 | import android.util.AttributeSet; 7 | import android.util.DisplayMetrics; 8 | import android.view.Gravity; 9 | import android.view.LayoutInflater; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | import android.view.WindowManager; 13 | import android.widget.FrameLayout; 14 | 15 | /** 16 | * 项目名称:androidUITraFloatLayout 17 | * 类描述: 18 | * 创建人:wengyiming 19 | * 创建时间:2016/9/14 17:12 20 | * 修改人:wengyiming 21 | * 修改时间:2016/9/14 17:12 22 | * 修改备注: 23 | */ 24 | public class FloatLayout extends FrameLayout implements View.OnTouchListener { 25 | //WindowMananger的params,控制这个值可以将自定义的view设置到窗口管理器中 26 | private WindowManager.LayoutParams mWmParams; 27 | private WindowManager mWindowManager;//当前view的窗口管理器 28 | private Context mContext;//一个全局上下文,demo使用的service的上下文 29 | 30 | private float mTouchStartX;//记录首次按下的位置x 31 | private float mTouchStartY;//记录首次按下的位置y 32 | 33 | private int mScreenWidth;//屏幕宽度 34 | private int mScreenHeight;//屏幕高度 35 | private boolean mDraging;//是否拖动中 36 | 37 | 38 | private FlHandler mFlHandler; 39 | 40 | public FloatLayout(Context context) { 41 | super(context); 42 | init(context); 43 | } 44 | 45 | public FloatLayout(Context context, AttributeSet attrs) { 46 | super(context, attrs); 47 | init(context); 48 | } 49 | 50 | public FloatLayout(Context context, AttributeSet attrs, int defStyleAttr) { 51 | super(context, attrs, defStyleAttr); 52 | init(context); 53 | } 54 | 55 | 56 | private void init(Context context) { 57 | this.mContext = context; 58 | initVM(); 59 | } 60 | 61 | 62 | public void setFlHandler(FlHandler flHandler) { 63 | mFlHandler = flHandler; 64 | View rootView = mFlHandler.onCreateView(LayoutInflater.from(mContext), this);//将xml布局导入当前View 65 | // rootView.setOnTouchListener(this); 66 | rootView.measure(MeasureSpec.makeMeasureSpec(0, 67 | MeasureSpec.UNSPECIFIED), MeasureSpec 68 | .makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 69 | addView(rootView); 70 | //将当前view设置为前置窗口 71 | bringToFront(); 72 | mWindowManager.addView(this, mWmParams); 73 | mFlHandler.onViewCreated(this); 74 | } 75 | 76 | public void attachToView(View view){ 77 | view.setOnTouchListener(this); 78 | } 79 | 80 | 81 | 82 | private void initVM() { 83 | mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);//获取系统的窗口服务 84 | DisplayMetrics dm = new DisplayMetrics(); 85 | mWindowManager.getDefaultDisplay().getMetrics(dm); 86 | mScreenWidth = dm.widthPixels;//根据当前屏幕信息拿到屏幕的宽高 87 | mScreenHeight = dm.heightPixels; 88 | 89 | this.mWmParams = new WindowManager.LayoutParams();//获取窗口参数 90 | 91 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 92 | mWmParams.type = WindowManager.LayoutParams.TYPE_TOAST;//等于API19或API19以下需要指定窗口参数type值为TYPE_TOAST才可以作为悬浮控件显示出来 93 | } else { 94 | mWmParams.type = WindowManager.LayoutParams.TYPE_PHONE;//API19以上侧只需指定为TYPE_PHONE即可 95 | } 96 | mWmParams.format = PixelFormat.RGBA_8888;//当前窗口的像素格式为RGBA_8888,即为最高质量 97 | 98 | 99 | //NOT_FOCUSABLE可以是悬浮控件可以响应事件,LAYOUT_IN_SCREEN可以指定悬浮球指定在屏幕内,部分虚拟按键的手机,虚拟按键隐藏时,虚拟按键的位置则属于屏幕内,此时悬浮球会出现在原虚拟按键的位置 100 | mWmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 101 | 102 | //默认指定位置在屏幕的左上方,可以根据需要自己修改 103 | mWmParams.gravity = Gravity.LEFT | Gravity.TOP; 104 | 105 | //默认指定的横坐标为屏幕最左边 106 | mWmParams.x = 0; 107 | 108 | //默认指定的纵坐标为屏幕高度的10分之一,这里只是大概约束,因为上的flags参数限制,悬浮球不会出现在屏幕外 109 | mWmParams.y = mScreenHeight / 10; 110 | 111 | //宽度指定为内容自适应 112 | mWmParams.width = LayoutParams.WRAP_CONTENT; 113 | mWmParams.height = LayoutParams.WRAP_CONTENT; 114 | } 115 | 116 | @Override 117 | public boolean onTouch(View view, MotionEvent event) { 118 | int x = (int) event.getRawX(); 119 | int y = (int) event.getRawY(); 120 | 121 | switch (event.getAction()) { 122 | case MotionEvent.ACTION_DOWN: 123 | mTouchStartX = event.getX(); 124 | mTouchStartY = event.getY(); 125 | mWmParams.alpha = 1f; 126 | mWindowManager.updateViewLayout(this, mWmParams); 127 | mDraging = false; 128 | break; 129 | case MotionEvent.ACTION_MOVE: 130 | float mMoveStartX = event.getX(); 131 | float mMoveStartY = event.getY(); 132 | 133 | float deltX = Math.abs(mTouchStartX - mMoveStartX); 134 | float deltY = Math.abs(mTouchStartY - mMoveStartY); 135 | if (deltX > 3 && deltY > 3) { 136 | mDraging = true; 137 | mWmParams.x = (int) (x - mTouchStartX); 138 | mWmParams.y = (int) (y - mTouchStartY); 139 | mWindowManager.updateViewLayout(this, mWmParams); 140 | mFlHandler.onFloatMoving(mDraging, deltX > 3 ? deltX : 0, deltY > 3 ? deltY : 0); 141 | return true; 142 | } 143 | break; 144 | case MotionEvent.ACTION_UP: 145 | mDraging = false; 146 | case MotionEvent.ACTION_CANCEL: 147 | if (mWmParams.x >= mScreenWidth / 2) { 148 | mWmParams.x = mScreenWidth; 149 | mFlHandler.onMoveCompletedLocation(true); 150 | } else if (mWmParams.x < mScreenWidth / 2) { 151 | mWmParams.x = 0; 152 | mFlHandler.onMoveCompletedLocation(false); 153 | } 154 | mWindowManager.updateViewLayout(this, mWmParams); 155 | mTouchStartX = mTouchStartY = 0; 156 | return false; 157 | } 158 | return false; 159 | } 160 | 161 | 162 | } 163 | -------------------------------------------------------------------------------- /app/src/main/java/com/ym/floatmenu/path/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path.demo; 2 | 3 | import com.ym.floatmenu.path.PathMenu; 4 | 5 | import android.os.Bundle; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.View; 8 | 9 | 10 | public class MainActivity extends AppCompatActivity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | PathMenu pathMenu = new PathMenu(this.getApplicationContext()); 16 | pathMenu.setVisibility(View.VISIBLE); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/composer_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/drawable/composer_camera.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/composer_music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/drawable/composer_music.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/composer_place.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/drawable/composer_place.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/composer_sleep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/drawable/composer_sleep.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/composer_thought.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/drawable/composer_thought.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/composer_with.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/drawable/composer_with.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/drawable/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 19 | 20 | 24 | 25 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FloatPathMenuSample 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/ym/floatmenu/path/demo/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path.demo; 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 | } -------------------------------------------------------------------------------- /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 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /floatpathmenu/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /floatpathmenu/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion '24' 6 | 7 | defaultConfig { 8 | minSdkVersion 9 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | testCompile 'junit:junit:4.12' 24 | compile 'com.android.support:appcompat-v7:23.3.0' 25 | } 26 | -------------------------------------------------------------------------------- /floatpathmenu/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\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 | -------------------------------------------------------------------------------- /floatpathmenu/src/androidTest/java/com/ym/floatmenu/path/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path; 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 | } -------------------------------------------------------------------------------- /floatpathmenu/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /floatpathmenu/src/main/java/com/ym/floatmenu/path/ArcPathMenu.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.PixelFormat; 9 | import android.graphics.RectF; 10 | import android.graphics.drawable.BitmapDrawable; 11 | import android.graphics.drawable.Drawable; 12 | import android.os.Build; 13 | import android.util.AttributeSet; 14 | import android.util.DisplayMetrics; 15 | import android.view.Gravity; 16 | import android.view.MotionEvent; 17 | import android.view.View; 18 | import android.view.WindowManager; 19 | import android.widget.FrameLayout; 20 | 21 | /** 22 | * 项目名称:FloatPathMenuSample 23 | * 类描述: 24 | * 创建人:wengyiming 25 | * 创建时间:2016/9/20 15:35 26 | * 修改人:wengyiming 27 | * 修改时间:2016/9/20 15:35 28 | * 修改备注: 29 | */ 30 | 31 | public class ArcPathMenu extends View implements View.OnTouchListener { 32 | 33 | private static final int MAX_DURATION = 3000; //最大间隔时长 34 | private static final int MIN_DURATION = 500; //最小间隔时长 35 | private static final int MIN_RADIUS = 100; 36 | 37 | public static final int LEFT_TOP = 1; 38 | public static final int CENTER_TOP = 2; 39 | public static final int RIGHT_TOP = 3; 40 | public static final int LEFT_CENTER = 4; 41 | public static final int CENTER = 5; 42 | public static final int RIGHT_CENTER = 6; 43 | public static final int LEFT_BOTTOM = 7; 44 | public static final int CENTER_BOTTOM = 8; 45 | public static final int RIGHT_BOTTOM = 9; 46 | 47 | 48 | private static final int[] mColors = new int[]{0xB07ECBDA, 0xB0E6A92C, 0xB0D6014D, 0xB05ABA94}; 49 | private static final int[] ITEM_DRAWABLES = {R.drawable.composer_camera, 50 | R.drawable.composer_music, R.drawable.composer_place, 51 | R.drawable.composer_sleep, R.drawable.composer_thought, 52 | R.drawable.composer_with}; 53 | 54 | 55 | private WindowManager mWindowManager;// 当前view的窗口管理器 56 | private WindowManager.LayoutParams mWmParams; // WindowMananger的params,控制这个值可以将自定义的view设置到窗口管理器中 57 | private float mTouchStartX;// 记录首次按下的位置x 58 | private float mTouchStartY;// 记录首次按下的位置y 59 | private int mScreenWidth;// 屏幕宽度 60 | private int mScreenHeight;// 屏幕高度 61 | private boolean mDraging;// 是否拖动中 62 | 63 | private int position;// 按钮的位置 64 | private int mRadius = MIN_RADIUS;// 中心菜单圆点到子菜单中心的距离 65 | private boolean mExpanded = false; 66 | private Bitmap logo; 67 | 68 | 69 | private Paint mPaint; 70 | private int mCanvasAngle; //Canvas旋转角度 71 | private int mDuration = MIN_DURATION;//动画间隔时长 72 | private Context mContext; 73 | 74 | public ArcPathMenu(Context context) { 75 | super(context); 76 | init(context); 77 | } 78 | 79 | public ArcPathMenu(Context context, AttributeSet attrs) { 80 | super(context, attrs); 81 | init(context); 82 | } 83 | 84 | public ArcPathMenu(Context context, AttributeSet attrs, int defStyleAttr) { 85 | super(context, attrs, defStyleAttr); 86 | init(context); 87 | } 88 | 89 | private void init(Context context) { 90 | this.mContext = context; 91 | initWindowsManager(context); 92 | Drawable drawable = context.getResources().getDrawable(R.drawable.composer_icn_plus); 93 | BitmapDrawable bd = (BitmapDrawable) drawable; 94 | logo = bd.getBitmap(); 95 | 96 | mPaint = new Paint(); 97 | mPaint.setAntiAlias(true); 98 | mPaint.setColor(mColors[0]); 99 | mPaint.setStrokeCap(Paint.Cap.ROUND); 100 | 101 | mRadius = logo.getWidth() * 2 + mRadius; 102 | 103 | } 104 | 105 | @Override 106 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 107 | int sizeW = 0; 108 | int sizeH = 0; 109 | 110 | sizeW = mRadius; 111 | sizeH = mRadius; 112 | // switch (position) { 113 | // case LEFT_TOP: 114 | // sizeW = mRadius; 115 | // sizeH = mRadius; 116 | // break; 117 | // case LEFT_CENTER: 118 | // sizeW = mRadius; 119 | // sizeH = mRadius; 120 | // break; 121 | // case LEFT_BOTTOM: 122 | // sizeW = mRadius; 123 | // sizeH = mRadius; 124 | // break; 125 | // default: 126 | // sizeW = mRadius; 127 | // sizeH = mRadius; 128 | // break; 129 | // } 130 | setMeasuredDimension(sizeW, sizeH); 131 | } 132 | 133 | @Override 134 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 135 | int centerX = getWidth() / 2; 136 | int centerY = getHeight() / 2; 137 | 138 | left = centerX - logo.getWidth() / 2; 139 | right = centerX + logo.getWidth() / 2; 140 | top = centerY - logo.getHeight() / 2; 141 | bottom = centerY + logo.getHeight() / 2; 142 | super.onLayout(changed, left, top, right, bottom); 143 | 144 | } 145 | 146 | @Override 147 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 148 | super.onSizeChanged(w, h, oldw, oldh); 149 | } 150 | 151 | @Override 152 | protected void onDraw(Canvas canvas) { 153 | super.onDraw(canvas); 154 | int centerX = getWidth() / 2; 155 | int centerY = getHeight() / 2; 156 | canvas.save(); 157 | 158 | int left = centerX - getWidth() / 2; 159 | int top = centerY - logo.getHeight() / 2; 160 | canvas.drawBitmap(logo, left, top, mPaint); 161 | canvas.restore(); 162 | 163 | 164 | canvas.save(); 165 | mPaint.setColor(Color.GREEN); 166 | mPaint.setStyle(Paint.Style.STROKE); 167 | 168 | 169 | RectF rect = new RectF(centerX - getWidth() / 2, centerX - getWidth() / 2, 170 | centerY + getWidth() / 2, centerY + getWidth() / 2); 171 | 172 | canvas.drawArc(rect, 270, 180, false, mPaint); 173 | canvas.restore(); 174 | 175 | // for (int subMenuImg : ITEM_DRAWABLES) { 176 | // Drawable drawable = mContext.getResources().getDrawable(subMenuImg); 177 | // BitmapDrawable bd = (BitmapDrawable) drawable; 178 | // Bitmap subMenuBitmap = bd.getBitmap(); 179 | // 180 | // Rect frame = computeChildFrame(left, top, mRadius, degrees, 181 | // logo.getWidth()); 182 | // 183 | // degrees += perDegrees; 184 | // canvas.drawBitmap(subMenuBitmap, frame.left, frame.top, mPaint); 185 | // } 186 | } 187 | 188 | private void initWindowsManager(Context context) { 189 | mWindowManager = (WindowManager) context 190 | .getSystemService(Context.WINDOW_SERVICE);// 获取系统的窗口服务 191 | DisplayMetrics dm = new DisplayMetrics(); 192 | mWindowManager.getDefaultDisplay().getMetrics(dm); 193 | mScreenWidth = dm.widthPixels;// 根据当前屏幕信息拿到屏幕的宽高 194 | mScreenHeight = dm.heightPixels; 195 | this.mWmParams = new WindowManager.LayoutParams();// 获取窗口参数 196 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 197 | mWmParams.type = WindowManager.LayoutParams.TYPE_TOAST;// 等于API19或API19以下需要指定窗口参数type值为TYPE_TOAST才可以作为悬浮控件显示出来 198 | } else { 199 | mWmParams.type = WindowManager.LayoutParams.TYPE_PHONE;// API19以上侧只需指定为TYPE_PHONE即可 200 | } 201 | 202 | 203 | mWmParams.format = PixelFormat.RGBA_8888;// 当前窗口的像素格式为RGBA_8888,即为最高质量 204 | 205 | // NOT_FOCUSABLE可以是悬浮控件可以响应事件,LAYOUT_IN_SCREEN可以指定悬浮球指定在屏幕内,部分虚拟按键的手机,虚拟按键隐藏时,虚拟按键的位置则属于屏幕内,此时悬浮球会出现在原虚拟按键的位置 206 | mWmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 207 | // | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 208 | // 默认指定位置在屏幕的左上方,可以根据需要自己修改 209 | mWmParams.gravity = Gravity.LEFT | Gravity.TOP; 210 | position = 1; 211 | // 默认指定的横坐标为屏幕一半 212 | mWmParams.x = 0; 213 | // 默认指定的纵坐标为屏幕高度的一半,这里只是大概约束,因为上的flags参数限制,悬浮球不会出现在屏幕外 214 | mWmParams.y = 0; 215 | 216 | // 宽度指定为内容自适应 217 | mWmParams.width = FrameLayout.LayoutParams.WRAP_CONTENT; 218 | mWmParams.height = FrameLayout.LayoutParams.WRAP_CONTENT; 219 | setBackgroundColor(context.getColor(android.R.color.holo_red_light)); 220 | mWindowManager.addView(this, mWmParams); 221 | 222 | setOnTouchListener(this); 223 | } 224 | 225 | /** 226 | * 触摸事件 227 | */ 228 | @Override 229 | public boolean onTouch(View v, MotionEvent event) { 230 | float x = (int) event.getRawX(); 231 | float y = (int) event.getRawY(); 232 | switch (event.getAction()) { 233 | case MotionEvent.ACTION_DOWN: 234 | mTouchStartX = event.getX(); 235 | mTouchStartY = event.getY(); 236 | mWmParams.alpha = 1f; 237 | mWindowManager.updateViewLayout(this, mWmParams); 238 | mDraging = false; 239 | break; 240 | 241 | case MotionEvent.ACTION_MOVE: 242 | float mMoveStartX = event.getX(); 243 | float mMoveStartY = event.getY(); 244 | if (Math.abs(mTouchStartX - mMoveStartX) > 3 245 | && Math.abs(mTouchStartY - mMoveStartY) > 3) { 246 | mDraging = true; 247 | mWmParams.x = (int) (x - mTouchStartX); 248 | mWmParams.y = (int) (y - mTouchStartY); 249 | mWindowManager.updateViewLayout(this, mWmParams); 250 | return false; 251 | } 252 | break; 253 | 254 | case MotionEvent.ACTION_UP: 255 | case MotionEvent.ACTION_CANCEL: 256 | refreshMenuXY(); 257 | break; 258 | } 259 | return false; 260 | } 261 | 262 | private void refreshMenuXY() { 263 | int wmX = mWmParams.x; 264 | int wmY = mWmParams.y; 265 | int halfLayoutW = 0; 266 | int halfLayoutH = 0; 267 | 268 | if (wmX < (mScreenWidth / 3)) //左边 竖区域 269 | { 270 | mWmParams.x = 0; 271 | if (wmY < mScreenHeight / 3) { 272 | position = LEFT_TOP;//左上 273 | mWmParams.y = 0; 274 | } else if (wmY >= mScreenHeight / 3 275 | && wmY <= mScreenHeight * 2 / 3) { 276 | position = LEFT_CENTER;//左中 277 | mWmParams.y = mScreenHeight / 2 - halfLayoutH; 278 | } else if (wmY > mScreenHeight * 2 / 3 && wmY <= mScreenHeight) { 279 | position = LEFT_BOTTOM;//左下 280 | mWmParams.y = mScreenHeight; 281 | } 282 | } else if (wmX >= mScreenWidth / 3 && wmX <= mScreenWidth * 2 / 3)//中间 竖区域 283 | { 284 | mWmParams.x = mScreenWidth / 2 - halfLayoutW; 285 | if (wmY < mScreenHeight / 3) { 286 | position = CENTER_TOP;//中上 287 | mWmParams.y = 0; 288 | } else if (wmY >= mScreenHeight / 3 289 | && wmY <= mScreenHeight * 2 / 3) { 290 | position = CENTER;//中 291 | mWmParams.y = mScreenHeight - halfLayoutH; 292 | } else if (wmY > mScreenHeight * 2 / 3) { 293 | position = CENTER_BOTTOM;//中下 294 | mWmParams.y = mScreenHeight; 295 | } 296 | } else if (wmX > mScreenWidth * 2 / 3 && wmX <= mScreenWidth)//右边竖区域 297 | { 298 | mWmParams.x = mScreenWidth; 299 | if (wmY < mScreenHeight / 3) { 300 | position = RIGHT_TOP;//上右 301 | mWmParams.y = 0; 302 | } else if (wmY >= mScreenHeight / 3 303 | && wmY <= mScreenHeight * 2 / 3) { 304 | position = RIGHT_CENTER;//中右 305 | mWmParams.y = mScreenHeight / 2 - halfLayoutH; 306 | } else if (wmY > mScreenHeight * 2 / 3 && wmY <= mScreenHeight) { 307 | position = RIGHT_BOTTOM;//下右 308 | mWmParams.y = mScreenHeight; 309 | } 310 | } 311 | mWindowManager.updateViewLayout(this, mWmParams); 312 | mTouchStartX = mTouchStartY = 0; 313 | } 314 | 315 | } 316 | -------------------------------------------------------------------------------- /floatpathmenu/src/main/java/com/ym/floatmenu/path/PathMenu.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.PixelFormat; 6 | import android.os.Build; 7 | import android.util.AttributeSet; 8 | import android.util.DisplayMetrics; 9 | import android.view.Gravity; 10 | import android.view.LayoutInflater; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | import android.view.View.OnTouchListener; 14 | import android.view.WindowManager; 15 | import android.view.animation.AlphaAnimation; 16 | import android.view.animation.Animation; 17 | import android.view.animation.Animation.AnimationListener; 18 | import android.view.animation.AnimationSet; 19 | import android.view.animation.DecelerateInterpolator; 20 | import android.view.animation.RotateAnimation; 21 | import android.view.animation.ScaleAnimation; 22 | import android.widget.FrameLayout; 23 | import android.widget.ImageView; 24 | import android.widget.Toast; 25 | 26 | /** 27 | * 自定义菜单 28 | * 29 | * @author 何凌波 30 | */ 31 | public class PathMenu extends FrameLayout implements OnTouchListener { 32 | FrameLayout controlLayout; 33 | private PathMenuLayout mPathMenuLayout; 34 | private ImageView mHintView;// 中心按钮显示图片 35 | private WindowManager mWindowManager;// 当前view的窗口管理器 36 | // WindowMananger的params,控制这个值可以将自定义的view设置到窗口管理器中 37 | private WindowManager.LayoutParams mWmParams; 38 | private float mTouchStartX;// 记录首次按下的位置x 39 | private float mTouchStartY;// 记录首次按下的位置y 40 | 41 | private int mScreenWidth;// 屏幕宽度 42 | private int mScreenHeight;// 屏幕高度 43 | private boolean mDraging;// 是否拖动中 44 | 45 | private Context mContext; 46 | 47 | private int position;// 按钮的位置 48 | public static final int LEFT_TOP = 1; 49 | public static final int CENTER_TOP = 2; 50 | public static final int RIGHT_TOP = 3; 51 | public static final int LEFT_CENTER = 4; 52 | public static final int CENTER = 5; 53 | public static final int RIGHT_CENTER = 6; 54 | public static final int LEFT_BOTTOM = 7; 55 | public static final int CENTER_BOTTOM = 8; 56 | public static final int RIGHT_BOTTOM = 9; 57 | 58 | 59 | private static final int[] ITEM_DRAWABLES = {R.drawable.composer_camera, 60 | R.drawable.composer_music, R.drawable.composer_place, 61 | R.drawable.composer_sleep, R.drawable.composer_thought, 62 | R.drawable.composer_with}; 63 | 64 | public PathMenu(Context context) { 65 | super(context); 66 | this.mContext = context; 67 | init(mContext); 68 | } 69 | 70 | public PathMenu(Context context, AttributeSet attrs) { 71 | super(context, attrs); 72 | this.mContext = context; 73 | init(mContext); 74 | applyAttrs(attrs); 75 | } 76 | 77 | 78 | public static class Builder { 79 | private int[] mDrables; 80 | private boolean showInFullSceen = true; 81 | } 82 | 83 | /** 84 | * 初始化中心按钮布局,加载布局文件,设置触摸事件 85 | */ 86 | private void init(Context context) { 87 | LayoutInflater li = (LayoutInflater) context 88 | .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 89 | li.inflate(R.layout.float_menu, this); 90 | setClipToPadding(false); 91 | setClipToPadding(false); 92 | mWindowManager = (WindowManager) context 93 | .getSystemService(Context.WINDOW_SERVICE);// 获取系统的窗口服务 94 | DisplayMetrics dm = new DisplayMetrics(); 95 | mWindowManager.getDefaultDisplay().getMetrics(dm); 96 | mScreenWidth = dm.widthPixels;// 根据当前屏幕信息拿到屏幕的宽高 97 | mScreenHeight = dm.heightPixels - getStatusBarHeight(); 98 | 99 | this.mWmParams = new WindowManager.LayoutParams();// 获取窗口参数 100 | 101 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 102 | mWmParams.type = WindowManager.LayoutParams.TYPE_TOAST;// 等于API19或API19以下需要指定窗口参数type值为TYPE_TOAST才可以作为悬浮控件显示出来 103 | } else { 104 | mWmParams.type = WindowManager.LayoutParams.TYPE_PHONE;// API19以上侧只需指定为TYPE_PHONE即可 105 | } 106 | mWmParams.format = PixelFormat.RGBA_8888;// 当前窗口的像素格式为RGBA_8888,即为最高质量 107 | 108 | // NOT_FOCUSABLE可以是悬浮控件可以响应事件,LAYOUT_IN_SCREEN可以指定悬浮球指定在屏幕内,部分虚拟按键的手机,虚拟按键隐藏时,虚拟按键的位置则属于屏幕内,此时悬浮球会出现在原虚拟按键的位置 109 | mWmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 110 | | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 111 | // 默认指定位置在屏幕的左上方,可以根据需要自己修改 112 | mWmParams.gravity = Gravity.LEFT | Gravity.TOP; 113 | position = 1; 114 | // 默认指定的横坐标为屏幕一半 115 | mWmParams.x = 0; 116 | // 默认指定的纵坐标为屏幕高度的一半,这里只是大概约束,因为上的flags参数限制,悬浮球不会出现在屏幕外 117 | mWmParams.y = 0; 118 | 119 | // 宽度指定为内容自适应 120 | mWmParams.width = LayoutParams.WRAP_CONTENT; 121 | mWmParams.height = LayoutParams.WRAP_CONTENT; 122 | 123 | mPathMenuLayout = (PathMenuLayout) findViewById(R.id.item_layout); 124 | controlLayout = (FrameLayout) findViewById(R.id.control_layout); 125 | controlLayout.setClickable(true); 126 | controlLayout.setOnTouchListener(this); 127 | 128 | mHintView = (ImageView) findViewById(R.id.control_hint); 129 | mWindowManager.addView(this, mWmParams); 130 | initPathMenu(this, ITEM_DRAWABLES);// 初始化子菜单 131 | controlLayout.setOnClickListener(new OnClickListener() { 132 | @Override 133 | public void onClick(View v) { 134 | if (!mDraging) { 135 | mHintView 136 | .startAnimation(createHintSwitchAnimation(mPathMenuLayout 137 | .isExpanded())); 138 | mPathMenuLayout.switchState(true, position); 139 | } 140 | } 141 | }); 142 | } 143 | 144 | /** 145 | * 应用自定义属性,设置弧度、子菜单项大小 146 | */ 147 | private void applyAttrs(AttributeSet attrs) { 148 | if (attrs != null) { 149 | TypedArray a = getContext().obtainStyledAttributes(attrs, 150 | R.styleable.ArcLayout, 0, 0); 151 | 152 | float fromDegrees = a.getFloat(R.styleable.ArcLayout_fromDegrees, 153 | PathMenuLayout.DEFAULT_FROM_DEGREES); 154 | float toDegrees = a.getFloat(R.styleable.ArcLayout_toDegrees, 155 | PathMenuLayout.DEFAULT_TO_DEGREES); 156 | mPathMenuLayout.setArc(fromDegrees, toDegrees); 157 | 158 | int defaultChildSize = mPathMenuLayout.getChildSize(); 159 | 160 | int newChildSize = a.getDimensionPixelSize( 161 | R.styleable.ArcLayout_childSize, defaultChildSize); 162 | mPathMenuLayout.setChildSize(newChildSize); 163 | a.recycle(); 164 | } 165 | } 166 | 167 | /** 168 | * 添加子菜单项和对应的点击事件 169 | */ 170 | public void addItem(View item, OnClickListener listener) { 171 | mPathMenuLayout.addView(item); 172 | item.setOnClickListener(getItemClickListener(listener)); 173 | } 174 | 175 | /** 176 | * 子菜单项的点击事件 177 | */ 178 | private OnClickListener getItemClickListener(final OnClickListener listener) { 179 | return new OnClickListener() { 180 | // 点击,child放大,其他child消失 181 | @Override 182 | public void onClick(final View viewClicked) { 183 | Animation animation = bindItemAnimation(viewClicked, true, 400); 184 | animation.setAnimationListener(new AnimationListener() { 185 | 186 | @Override 187 | public void onAnimationStart(Animation animation) { 188 | 189 | } 190 | 191 | @Override 192 | public void onAnimationRepeat(Animation animation) { 193 | 194 | } 195 | 196 | @Override 197 | public void onAnimationEnd(Animation animation) { 198 | postDelayed(new Runnable() { 199 | 200 | @Override 201 | public void run() { 202 | itemDidDisappear(); 203 | } 204 | }, 0); 205 | } 206 | }); 207 | // child 被点击的时候,自身放大,其他child消失的效果 208 | final int itemCount = mPathMenuLayout.getChildCount(); 209 | for (int i = 0; i < itemCount; i++) { 210 | View item = mPathMenuLayout.getChildAt(i); 211 | if (viewClicked != item) { 212 | bindItemAnimation(item, false, 300); 213 | } 214 | } 215 | // 中心控制点动画 旋转 216 | mPathMenuLayout.invalidate(); 217 | mHintView.startAnimation(createHintSwitchAnimation(true)); 218 | 219 | if (listener != null) { 220 | listener.onClick(viewClicked); 221 | } 222 | } 223 | }; 224 | } 225 | 226 | /** 227 | * 绑定子菜单项动画 228 | */ 229 | private Animation bindItemAnimation(final View child, 230 | final boolean isClicked, final long duration) { 231 | Animation animation = createItemDisapperAnimation(duration, isClicked); 232 | child.setAnimation(animation); 233 | 234 | return animation; 235 | } 236 | 237 | /** 238 | * 子菜单项关闭时消失 239 | */ 240 | private void itemDidDisappear() { 241 | final int itemCount = mPathMenuLayout.getChildCount(); 242 | for (int i = 0; i < itemCount; i++) { 243 | View item = mPathMenuLayout.getChildAt(i); 244 | item.clearAnimation(); 245 | } 246 | 247 | mPathMenuLayout.switchState(false, position); 248 | } 249 | 250 | /** 251 | * 子菜单消失动画 252 | */ 253 | private static Animation createItemDisapperAnimation(final long duration, 254 | final boolean isClicked) { 255 | AnimationSet animationSet = new AnimationSet(true); 256 | // 放大缩小动画,isClicked 为true,放大两倍;为false,缩小至0 257 | animationSet.addAnimation(new ScaleAnimation(1.0f, isClicked ? 2.0f 258 | : 0.0f, 1.0f, isClicked ? 2.0f : 0.0f, 259 | Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 260 | 0.5f)); 261 | // Alpha改为0,child消失 262 | animationSet.addAnimation(new AlphaAnimation(1.0f, 0.0f)); 263 | 264 | animationSet.setDuration(duration); 265 | animationSet.setInterpolator(new DecelerateInterpolator()); 266 | animationSet.setFillAfter(true); 267 | 268 | return animationSet; 269 | } 270 | 271 | /** 272 | * 中心按钮点击旋转45度动画 273 | */ 274 | private static Animation createHintSwitchAnimation(final boolean expanded) { 275 | Animation animation = new RotateAnimation(expanded ? 45 : 0, 276 | expanded ? 0 : 45, Animation.RELATIVE_TO_SELF, 0.5f, 277 | Animation.RELATIVE_TO_SELF, 0.5f); 278 | animation.setStartOffset(0); 279 | animation.setDuration(100); 280 | animation.setInterpolator(new DecelerateInterpolator()); 281 | animation.setFillAfter(true); 282 | 283 | return animation; 284 | } 285 | 286 | /** 287 | * 触摸事件 288 | */ 289 | @Override 290 | public boolean onTouch(View v, MotionEvent event) { 291 | float x = (int) event.getRawX(); 292 | float y = (int) event.getRawY(); 293 | switch (event.getAction()) { 294 | case MotionEvent.ACTION_DOWN: 295 | mTouchStartX = event.getX(); 296 | mTouchStartY = event.getY(); 297 | mWmParams.alpha = 1f; 298 | mWindowManager.updateViewLayout(this, mWmParams); 299 | mDraging = false; 300 | break; 301 | 302 | case MotionEvent.ACTION_MOVE: 303 | float mMoveStartX = event.getX(); 304 | float mMoveStartY = event.getY(); 305 | if (Math.abs(mTouchStartX - mMoveStartX) > 3 306 | && Math.abs(mTouchStartY - mMoveStartY) > 3) { 307 | // if (mPathMenuLayout.isExpanded()) { 308 | // mPathMenuLayout.switchState(false); 309 | // } 310 | mDraging = true; 311 | mWmParams.x = (int) (x - mTouchStartX); 312 | mWmParams.y = (int) (y - mTouchStartY); 313 | mWindowManager.updateViewLayout(this, mWmParams); 314 | return false; 315 | } 316 | break; 317 | 318 | case MotionEvent.ACTION_UP: 319 | case MotionEvent.ACTION_CANCEL: 320 | int wmX = mWmParams.x; 321 | int wmY = mWmParams.y; 322 | if (wmX <= mScreenWidth / 3) //左边 竖区域 323 | { 324 | mWmParams.x = 0; 325 | if (wmY <= mScreenHeight / 3) { 326 | position = LEFT_TOP;//左上 327 | mWmParams.y = 0; 328 | } else if (wmY > mScreenHeight / 3 329 | && wmY < mScreenHeight * 2 / 3) { 330 | position = LEFT_CENTER;//左中 331 | mWmParams.y = mScreenHeight / 2; 332 | } else if (wmY >= mScreenHeight * 2 / 3 && wmY < mScreenHeight) { 333 | position = LEFT_BOTTOM;//左下 334 | mWmParams.y = mScreenHeight; 335 | } 336 | } else if (wmX >= mScreenWidth / 3 && wmX <= mScreenWidth * 2 / 3)//中间 竖区域 337 | { 338 | mWmParams.x = mScreenWidth / 2; 339 | if (wmY <= mScreenHeight / 3) { 340 | position = CENTER_TOP;//中上 341 | mWmParams.y = 0; 342 | } else if (wmY > mScreenHeight / 3 343 | && wmY < mScreenHeight * 2 / 3) { 344 | position = CENTER;//中 345 | mWmParams.y = mScreenHeight / 2; 346 | } else if (wmY >= mScreenHeight * 2 / 3) { 347 | position = CENTER_BOTTOM;//中下 348 | mWmParams.y = mScreenHeight; 349 | } 350 | } else if (wmX >= mScreenWidth * 2 / 3 && wmX < mScreenWidth)//右边竖区域 351 | { 352 | mWmParams.x = mScreenWidth; 353 | if (wmY <= mScreenHeight / 3) { 354 | position = RIGHT_TOP;//上右 355 | mWmParams.y = 0; 356 | } else if (wmY > mScreenHeight / 3 357 | && wmY < mScreenHeight * 2 / 3) { 358 | position = RIGHT_CENTER;//中右 359 | mWmParams.y = mScreenHeight / 2; 360 | 361 | } else if (wmY >= mScreenHeight * 2 / 3 && wmY < mScreenHeight) { 362 | position = RIGHT_BOTTOM;//下右 363 | mWmParams.y = mScreenHeight; 364 | } 365 | } 366 | refreshPathMenu(position); 367 | mWindowManager.updateViewLayout(this, mWmParams); 368 | mTouchStartX = mTouchStartY = 0; 369 | break; 370 | } 371 | return false; 372 | } 373 | 374 | /** 375 | * 根据按钮位置改变子菜单方向 376 | */ 377 | private void refreshPathMenu(int position) { 378 | FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mPathMenuLayout.getLayoutParams(); 379 | FrameLayout.LayoutParams params1 = (FrameLayout.LayoutParams) controlLayout.getLayoutParams(); 380 | 381 | switch (position) { 382 | case PathMenu.LEFT_TOP://左上 383 | params1.gravity = Gravity.LEFT | Gravity.TOP; 384 | params.gravity = Gravity.LEFT | Gravity.TOP; 385 | mPathMenuLayout.setArc(0, 90, position); 386 | break; 387 | case PathMenu.LEFT_CENTER://左中 388 | params1.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; 389 | params.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; 390 | mPathMenuLayout.setArc(270, 270 + 180, position); 391 | break; 392 | case PathMenu.LEFT_BOTTOM://左下 393 | params1.gravity = Gravity.LEFT | Gravity.BOTTOM; 394 | params.gravity = Gravity.LEFT | Gravity.BOTTOM; 395 | mPathMenuLayout.setArc(270, 360, position); 396 | break; 397 | case PathMenu.RIGHT_TOP://右上 398 | params1.gravity = Gravity.RIGHT | Gravity.TOP; 399 | params.gravity = Gravity.RIGHT | Gravity.TOP; 400 | mPathMenuLayout.setArc(90, 180, position); 401 | break; 402 | case PathMenu.RIGHT_CENTER://右中 403 | params1.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; 404 | params.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; 405 | mPathMenuLayout.setArc(90, 270, position); 406 | break; 407 | case PathMenu.RIGHT_BOTTOM://右下 408 | params1.gravity = Gravity.BOTTOM | Gravity.RIGHT; 409 | params.gravity = Gravity.BOTTOM | Gravity.RIGHT; 410 | mPathMenuLayout.setArc(180, 270, position); 411 | break; 412 | 413 | case PathMenu.CENTER_TOP://上中 414 | params1.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; 415 | params.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; 416 | mPathMenuLayout.setArc(0, 180, position); 417 | break; 418 | case PathMenu.CENTER_BOTTOM://下中 419 | params1.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 420 | params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 421 | mPathMenuLayout.setArc(180, 360, position); 422 | break; 423 | case PathMenu.CENTER: 424 | params1.gravity = Gravity.CENTER; 425 | params.gravity = Gravity.CENTER; 426 | mPathMenuLayout.setArc(0, 360, position); 427 | break; 428 | } 429 | controlLayout.setLayoutParams(params1); 430 | mPathMenuLayout.setLayoutParams(params); 431 | } 432 | 433 | /** 434 | * 初始化子菜单图片、点击事件 435 | */ 436 | private void initPathMenu(PathMenu menu, int[] itemDrawables) { 437 | final int itemCount = itemDrawables.length; 438 | for (int i = 0; i < itemCount; i++) { 439 | ImageView item = new ImageView(mContext); 440 | item.setImageResource(itemDrawables[i]); 441 | final int index = i; 442 | menu.addItem(item, new OnClickListener() { 443 | 444 | @Override 445 | public void onClick(View v) { 446 | switch (index) { 447 | case 0: 448 | Toast.makeText(getContext(), "第0个被点击", 449 | Toast.LENGTH_SHORT).show(); 450 | break; 451 | 452 | case 1: 453 | Toast.makeText(getContext(), "第1个被点击", 454 | Toast.LENGTH_SHORT).show(); 455 | break; 456 | } 457 | } 458 | }); 459 | } 460 | } 461 | 462 | int statusBarHeight = 0; 463 | 464 | public int getStatusBarHeight() { 465 | if (statusBarHeight <= 0) { 466 | int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); 467 | if (resourceId > 0) { 468 | statusBarHeight = getResources().getDimensionPixelSize(resourceId); 469 | } 470 | } 471 | return statusBarHeight; 472 | } 473 | 474 | 475 | } -------------------------------------------------------------------------------- /floatpathmenu/src/main/java/com/ym/floatmenu/path/PathMenuActivity.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.view.View.OnClickListener; 8 | import android.widget.Button; 9 | import android.widget.ImageView; 10 | import android.widget.Toast; 11 | 12 | public class PathMenuActivity extends Activity { 13 | 14 | private static final int[] ITEM_DRAWABLES = { R.drawable.composer_camera, 15 | R.drawable.composer_music, R.drawable.composer_place, 16 | R.drawable.composer_sleep, R.drawable.composer_thought, 17 | R.drawable.composer_with }; 18 | 19 | private static final int[] ITEM_DRAWABLES2 = { R.drawable.composer_camera, 20 | R.drawable.composer_music, R.drawable.composer_place, 21 | R.drawable.composer_sleep, R.drawable.composer_thought, 22 | R.drawable.composer_with,R.drawable.composer_with,R.drawable.composer_with,R.drawable.composer_with,R.drawable.composer_with }; 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_float); 27 | Button btnOpen = (Button) findViewById(R.id.btnOpenFloat); 28 | Button btnClose = (Button) findViewById(R.id.btnCloseFloat); 29 | btnOpen.setOnClickListener(mClickListener); 30 | btnClose.setOnClickListener(mClickListener); 31 | PathMenu pathMenu = (PathMenu) findViewById(R.id.path_menu); 32 | PathMenu pathMenu2 = (PathMenu) findViewById(R.id.path_menu2); 33 | PathMenu pathMenu3 = (PathMenu) findViewById(R.id.path_menu3); 34 | PathMenu pathMenu4 = (PathMenu) findViewById(R.id.path_menu4); 35 | PathMenu pathMenu5 = (PathMenu) findViewById(R.id.path_menu5); 36 | PathMenu pathMenu6 = (PathMenu) findViewById(R.id.path_menu6); 37 | PathMenu pathMenu7 = (PathMenu) findViewById(R.id.path_menu7); 38 | PathMenu pathMenu8 = (PathMenu) findViewById(R.id.path_menu8); 39 | 40 | initPathMenu(pathMenu, ITEM_DRAWABLES); 41 | initPathMenu(pathMenu2, ITEM_DRAWABLES); 42 | initPathMenu(pathMenu3, ITEM_DRAWABLES); 43 | initPathMenu(pathMenu4, ITEM_DRAWABLES); 44 | initPathMenu(pathMenu5, ITEM_DRAWABLES); 45 | initPathMenu(pathMenu6, ITEM_DRAWABLES); 46 | initPathMenu(pathMenu7, ITEM_DRAWABLES); 47 | initPathMenu(pathMenu8, ITEM_DRAWABLES2); 48 | } 49 | private void initPathMenu(PathMenu menu, int[] itemDrawables) { 50 | final int itemCount = itemDrawables.length; 51 | for (int i = 0; i < itemCount; i++) { 52 | ImageView item = new ImageView(this); 53 | item.setImageResource(itemDrawables[i]); 54 | 55 | final int position = i; 56 | menu.addItem(item, new OnClickListener() { 57 | 58 | @Override 59 | public void onClick(View v) { 60 | Toast.makeText(PathMenuActivity.this, 61 | "position:" + position, Toast.LENGTH_SHORT).show(); 62 | } 63 | }); 64 | } 65 | } 66 | 67 | OnClickListener mClickListener = new OnClickListener() { 68 | 69 | @Override 70 | public void onClick(View v) { 71 | if (v.getId() == R.id.btnOpenFloat) { 72 | Intent intent = new Intent(PathMenuActivity.this, 73 | PathMenuService.class); 74 | startService(intent); 75 | } else if (v.getId() == R.id.btnCloseFloat) { 76 | Intent intent = new Intent(PathMenuActivity.this, 77 | PathMenuService.class); 78 | stopService(intent); 79 | } 80 | } 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /floatpathmenu/src/main/java/com/ym/floatmenu/path/PathMenuLayout.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Rect; 6 | import android.util.AttributeSet; 7 | import android.util.DisplayMetrics; 8 | import android.util.Log; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.view.animation.AccelerateInterpolator; 12 | import android.view.animation.Animation; 13 | import android.view.animation.Animation.AnimationListener; 14 | import android.view.animation.AnimationSet; 15 | import android.view.animation.Interpolator; 16 | import android.view.animation.LinearInterpolator; 17 | import android.view.animation.OvershootInterpolator; 18 | import android.view.animation.RotateAnimation; 19 | 20 | /** 21 | * 子菜单项布局 22 | * 23 | * @author 何凌波 24 | */ 25 | public class PathMenuLayout extends ViewGroup { 26 | private int mChildSize; // 子菜单项大小相同 27 | private int mChildPadding = 5; 28 | public static final float DEFAULT_FROM_DEGREES = 270.0f; 29 | public static final float DEFAULT_TO_DEGREES = 360.0f; 30 | private float mFromDegrees = DEFAULT_FROM_DEGREES; 31 | private float mToDegrees = DEFAULT_TO_DEGREES; 32 | private static final int MIN_RADIUS = 100; 33 | private int mRadius;// 中心菜单圆点到子菜单中心的距离 34 | private boolean mExpanded = false; 35 | 36 | private int position = PathMenu.LEFT_TOP; 37 | private int centerX = 0; 38 | private int centerY = 0; 39 | 40 | public void computeCenterXY(int position) { 41 | switch (position) { 42 | case PathMenu.LEFT_TOP://左上 43 | centerX = getWidth() / 2 - getRadiusAndPadding(); 44 | centerY = getHeight() / 2 - getRadiusAndPadding(); 45 | break; 46 | case PathMenu.LEFT_CENTER://左中 47 | centerX = getWidth() / 2 - getRadiusAndPadding(); 48 | centerY = getHeight() / 2; 49 | break; 50 | case PathMenu.LEFT_BOTTOM://左下 51 | centerX = getWidth() / 2 - getRadiusAndPadding(); 52 | centerY = getHeight() / 2 + getRadiusAndPadding(); 53 | break; 54 | case PathMenu.CENTER_TOP://上中 55 | centerX = getWidth() / 2; 56 | centerY = getHeight() / 2 - getRadiusAndPadding(); 57 | break; 58 | case PathMenu.CENTER_BOTTOM://下中 59 | centerX = getWidth() / 2; 60 | centerY = getHeight() / 2 + getRadiusAndPadding(); 61 | break; 62 | case PathMenu.RIGHT_TOP://右上 63 | centerX = getWidth() / 2 + getRadiusAndPadding(); 64 | centerY = getHeight() / 2 - getRadiusAndPadding(); 65 | break; 66 | case PathMenu.RIGHT_CENTER://右中 67 | centerX = getWidth() / 2 + getRadiusAndPadding(); 68 | centerY = getHeight() / 2; 69 | break; 70 | case PathMenu.RIGHT_BOTTOM://右下 71 | centerX = getWidth() / 2 + getRadiusAndPadding(); 72 | centerY = getHeight() / 2 + getRadiusAndPadding(); 73 | break; 74 | 75 | case PathMenu.CENTER: 76 | centerX = getWidth() / 2; 77 | centerY = getHeight() / 2; 78 | break; 79 | } 80 | } 81 | 82 | private int getRadiusAndPadding() { 83 | return mRadius + (mChildPadding * 2); 84 | } 85 | 86 | public PathMenuLayout(Context context, AttributeSet attrs) { 87 | super(context, attrs); 88 | // 获取自定义属性,设定默认值 89 | if (attrs != null) { 90 | TypedArray a = getContext().obtainStyledAttributes(attrs, 91 | R.styleable.ArcLayout, 0, 0); 92 | mFromDegrees = a.getFloat(R.styleable.ArcLayout_fromDegrees, 93 | DEFAULT_FROM_DEGREES); 94 | mToDegrees = a.getFloat(R.styleable.ArcLayout_toDegrees, 95 | DEFAULT_TO_DEGREES); 96 | mChildSize = Math 97 | .max(a.getDimensionPixelSize( 98 | R.styleable.ArcLayout_childSize, 0), 0); 99 | 100 | a.recycle(); 101 | } 102 | } 103 | 104 | /** 105 | * 计算半径 106 | */ 107 | private static int computeRadius(final float arcDegrees, 108 | final int childCount, final int childSize, final int childPadding, 109 | final int minRadius) { 110 | if (childCount < 2) { 111 | return minRadius; 112 | } 113 | // final float perDegrees = arcDegrees / (childCount - 1); 114 | 115 | 116 | final float perDegrees = arcDegrees == 360 ? (arcDegrees) / (childCount) : (arcDegrees) / (childCount - 1); 117 | 118 | 119 | final float perHalfDegrees = perDegrees / 2; 120 | final int perSize = childSize + childPadding; 121 | 122 | final int radius = (int) ((perSize / 2) / Math.sin(Math 123 | .toRadians(perHalfDegrees))); 124 | 125 | return Math.max(radius, minRadius); 126 | } 127 | 128 | /** 129 | * 计算子菜单项的范围 130 | */ 131 | private static Rect computeChildFrame(final int centerX, final int centerY, 132 | final int radius, final float degrees, final int size) { 133 | //子菜单项中心点 134 | final double childCenterX = centerX + radius 135 | * Math.cos(Math.toRadians(degrees)); 136 | final double childCenterY = centerY + radius 137 | * Math.sin(Math.toRadians(degrees)); 138 | //子菜单项的左上角,右上角,左下角,右下角 139 | return new Rect((int) (childCenterX - size / 2), 140 | (int) (childCenterY - size / 2), 141 | (int) (childCenterX + size / 2), 142 | (int) (childCenterY + size / 2)); 143 | } 144 | 145 | public int getRadius() { 146 | return mRadius; 147 | } 148 | 149 | 150 | /** 151 | * 子菜单项大小 152 | */ 153 | @Override 154 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 155 | DisplayMetrics dm = getResources().getDisplayMetrics(); 156 | int screenWidth = dm.widthPixels; 157 | int radius = mRadius = computeRadius( 158 | Math.abs(mToDegrees - mFromDegrees), getChildCount(), 159 | mChildSize, mChildPadding, MIN_RADIUS); 160 | Log.i("layout", "radius:" + radius); 161 | 162 | int layoutPadding = 10; 163 | int size = radius * 2 + mChildSize + mChildPadding 164 | + layoutPadding * 2; 165 | Log.i("layout", "size:" + size); 166 | 167 | setMeasuredDimension(size, size); 168 | 169 | final int count = getChildCount(); 170 | for (int i = 0; i < count; i++) { 171 | getChildAt(i) 172 | .measure( 173 | MeasureSpec.makeMeasureSpec(mChildSize, 174 | MeasureSpec.EXACTLY), 175 | MeasureSpec.makeMeasureSpec(mChildSize, 176 | MeasureSpec.EXACTLY)); 177 | } 178 | } 179 | 180 | /** 181 | * 子菜单项位置 182 | */ 183 | @Override 184 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 185 | // final int centerX = getWidth() / 2 - mRadius; 186 | // final int centerY = getHeight() / 2; 187 | computeCenterXY(position); 188 | //当子菜单要收缩时radius=0,在ViewGroup坐标中心 189 | final int radius = mExpanded ? mRadius : 0; 190 | 191 | final int childCount = getChildCount(); 192 | // final float perDegrees =Math.abs (mToDegrees - mFromDegrees) / (childCount - 1); 193 | final float perDegrees = Math.abs(mToDegrees - mFromDegrees) == 360 ? (Math.abs(mToDegrees - mFromDegrees)) / (childCount) : (Math.abs(mToDegrees - mFromDegrees)) / (childCount - 1); 194 | 195 | 196 | float degrees = mFromDegrees; 197 | for (int i = 0; i < childCount; i++) { 198 | Rect frame = computeChildFrame(centerX, centerY, radius, degrees, 199 | mChildSize); 200 | degrees += perDegrees; 201 | getChildAt(i).layout(frame.left, frame.top, frame.right, 202 | frame.bottom); 203 | } 204 | } 205 | 206 | /** 207 | * 计算动画开始时的偏移量 208 | */ 209 | private static long computeStartOffset(final int childCount, 210 | final boolean expanded, final int index, final float delayPercent, 211 | final long duration, Interpolator interpolator) { 212 | final float delay = delayPercent * duration; 213 | final long viewDelay = (long) (getTransformedIndex(expanded, 214 | childCount, index) * delay); 215 | final float totalDelay = delay * childCount; 216 | 217 | float normalizedDelay = viewDelay / totalDelay; 218 | normalizedDelay = interpolator.getInterpolation(normalizedDelay); 219 | 220 | return (long) (normalizedDelay * totalDelay); 221 | } 222 | 223 | /** 224 | * 变换时的子菜单项索引 225 | */ 226 | private static int getTransformedIndex(final boolean expanded, 227 | final int count, final int index) { 228 | if (expanded) { 229 | return count - 1 - index; 230 | } 231 | 232 | return index; 233 | } 234 | 235 | /** 236 | * 展开动画 237 | */ 238 | private static Animation createExpandAnimation(float fromXDelta, 239 | float toXDelta, float fromYDelta, float toYDelta, long startOffset, 240 | long duration, Interpolator interpolator) { 241 | Animation animation = new RotateAndTranslateAnimation(0, toXDelta, 0, 242 | toYDelta, 0, 720); 243 | animation.setStartOffset(startOffset); 244 | animation.setDuration(duration); 245 | animation.setInterpolator(interpolator); 246 | animation.setFillAfter(true); 247 | 248 | return animation; 249 | } 250 | 251 | /** 252 | * 收缩动画 253 | */ 254 | private static Animation createShrinkAnimation(float fromXDelta, 255 | float toXDelta, float fromYDelta, float toYDelta, long startOffset, 256 | long duration, Interpolator interpolator) { 257 | AnimationSet animationSet = new AnimationSet(false); 258 | animationSet.setFillAfter(true); 259 | //收缩过程中,child 逆时针自旋转360度 260 | final long preDuration = duration / 2; 261 | Animation rotateAnimation = new RotateAnimation(0, 360, 262 | Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 263 | 0.5f); 264 | rotateAnimation.setStartOffset(startOffset); 265 | rotateAnimation.setDuration(preDuration); 266 | rotateAnimation.setInterpolator(new LinearInterpolator()); 267 | rotateAnimation.setFillAfter(true); 268 | 269 | animationSet.addAnimation(rotateAnimation); 270 | //收缩过程中位移,并逆时针旋转360度 271 | Animation translateAnimation = new RotateAndTranslateAnimation(0, 272 | toXDelta, 0, toYDelta, 360, 720); 273 | translateAnimation.setStartOffset(startOffset + preDuration); 274 | translateAnimation.setDuration(duration - preDuration); 275 | translateAnimation.setInterpolator(interpolator); 276 | translateAnimation.setFillAfter(true); 277 | 278 | animationSet.addAnimation(translateAnimation); 279 | 280 | return animationSet; 281 | } 282 | 283 | /** 284 | * 绑定子菜单项动画 285 | */ 286 | private void bindChildAnimation(final View child, final int index, 287 | final long duration) { 288 | final boolean expanded = mExpanded; 289 | // final int centerX = getWidth() / 2 - mRadius; //ViewGroup的中心X坐标 290 | // final int centerY = getHeight() / 2; 291 | 292 | computeCenterXY(position); 293 | final int radius = expanded ? 0 : mRadius; 294 | 295 | final int childCount = getChildCount(); 296 | final float perDegrees = Math.abs(mToDegrees - mFromDegrees) == 360 ? (mToDegrees - mFromDegrees) / (childCount) : (mToDegrees - mFromDegrees) / (childCount - 1); 297 | Rect frame = computeChildFrame(centerX, centerY, radius, mFromDegrees 298 | + index * perDegrees, mChildSize); 299 | 300 | final int toXDelta = frame.left - child.getLeft();//展开或收缩动画,child沿X轴位移距离 301 | final int toYDelta = frame.top - child.getTop();//展开或收缩动画,child沿Y轴位移距离 302 | 303 | Interpolator interpolator = mExpanded ? new AccelerateInterpolator() 304 | : new OvershootInterpolator(1.5f); 305 | final long startOffset = computeStartOffset(childCount, mExpanded, 306 | index, 0.1f, duration, interpolator); 307 | 308 | //mExpanded为true,已经展开,收缩动画;为false,展开动画 309 | Animation animation = mExpanded ? createShrinkAnimation(0, toXDelta, 0, 310 | toYDelta, startOffset, duration, interpolator) 311 | : createExpandAnimation(0, toXDelta, 0, toYDelta, startOffset, 312 | duration, interpolator); 313 | 314 | final boolean isLast = getTransformedIndex(expanded, childCount, index) == childCount - 1; 315 | animation.setAnimationListener(new AnimationListener() { 316 | 317 | @Override 318 | public void onAnimationStart(Animation animation) { 319 | 320 | } 321 | 322 | @Override 323 | public void onAnimationRepeat(Animation animation) { 324 | 325 | } 326 | 327 | @Override 328 | public void onAnimationEnd(Animation animation) { 329 | if (isLast) { 330 | postDelayed(new Runnable() { 331 | 332 | @Override 333 | public void run() { 334 | onAllAnimationsEnd(); 335 | } 336 | }, 0); 337 | } 338 | } 339 | }); 340 | 341 | child.setAnimation(animation); 342 | } 343 | 344 | public boolean isExpanded() { 345 | return mExpanded; 346 | } 347 | 348 | 349 | /** 350 | * 设定弧度 351 | */ 352 | public void setArc(float fromDegrees, float toDegrees, int position) { 353 | this.position = position; 354 | if (mFromDegrees == fromDegrees && mToDegrees == toDegrees) { 355 | return; 356 | } 357 | 358 | mFromDegrees = fromDegrees; 359 | mToDegrees = toDegrees; 360 | computeCenterXY(position); 361 | requestLayout(); 362 | } 363 | 364 | /** 365 | * 设定弧度 366 | */ 367 | public void setArc(float fromDegrees, float toDegrees) { 368 | if (mFromDegrees == fromDegrees && mToDegrees == toDegrees) { 369 | return; 370 | } 371 | 372 | mFromDegrees = fromDegrees; 373 | mToDegrees = toDegrees; 374 | computeCenterXY(position); 375 | requestLayout(); 376 | } 377 | 378 | /** 379 | * 设定子菜单项大小 380 | */ 381 | public void setChildSize(int size) { 382 | if (mChildSize == size || size < 0) { 383 | return; 384 | } 385 | 386 | mChildSize = size; 387 | 388 | requestLayout(); 389 | } 390 | 391 | public int getChildSize() { 392 | return mChildSize; 393 | } 394 | 395 | 396 | /** 397 | * 切换中心按钮的展开缩小 398 | */ 399 | public void switchState(final boolean showAnimation, int position) { 400 | this.position = position; 401 | if (showAnimation) { 402 | final int childCount = getChildCount(); 403 | for (int i = 0; i < childCount; i++) { 404 | bindChildAnimation(getChildAt(i), i, 300); 405 | } 406 | } 407 | 408 | mExpanded = !mExpanded; 409 | 410 | if (!showAnimation) { 411 | requestLayout(); 412 | } 413 | 414 | invalidate(); 415 | } 416 | 417 | 418 | /** 419 | * 切换中心按钮的展开缩小 420 | */ 421 | public void switchState(final boolean showAnimation) { 422 | if (showAnimation) { 423 | final int childCount = getChildCount(); 424 | for (int i = 0; i < childCount; i++) { 425 | bindChildAnimation(getChildAt(i), i, 300); 426 | } 427 | } 428 | 429 | mExpanded = !mExpanded; 430 | 431 | if (!showAnimation) { 432 | requestLayout(); 433 | } 434 | 435 | invalidate(); 436 | } 437 | 438 | 439 | /** 440 | * 结束所有动画 441 | */ 442 | private void onAllAnimationsEnd() { 443 | final int childCount = getChildCount(); 444 | for (int i = 0; i < childCount; i++) { 445 | getChildAt(i).clearAnimation(); 446 | } 447 | 448 | requestLayout(); 449 | } 450 | } -------------------------------------------------------------------------------- /floatpathmenu/src/main/java/com/ym/floatmenu/path/PathMenuService.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.os.IBinder; 6 | 7 | 8 | public class PathMenuService extends Service { 9 | 10 | @Override 11 | public IBinder onBind(Intent intent) { 12 | return null; 13 | } 14 | @Override 15 | public void onCreate() { 16 | super.onCreate(); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /floatpathmenu/src/main/java/com/ym/floatmenu/path/RotateAndTranslateAnimation.java: -------------------------------------------------------------------------------- 1 | package com.ym.floatmenu.path; 2 | 3 | import android.view.animation.Animation; 4 | import android.view.animation.Transformation; 5 | /** 6 | * 自定义动画类 7 | * 控制对象的位置,以对象的中心旋转 8 | * @author 何凌波 9 | * 10 | */ 11 | public class RotateAndTranslateAnimation extends Animation { 12 | private int mFromXType = ABSOLUTE; 13 | 14 | private int mToXType = ABSOLUTE; 15 | 16 | private int mFromYType = ABSOLUTE; 17 | 18 | private int mToYType = ABSOLUTE; 19 | 20 | private float mFromXValue = 0.0f; 21 | 22 | private float mToXValue = 0.0f; 23 | 24 | private float mFromYValue = 0.0f; 25 | 26 | private float mToYValue = 0.0f; 27 | 28 | private float mFromXDelta; 29 | 30 | private float mToXDelta; 31 | 32 | private float mFromYDelta; 33 | 34 | private float mToYDelta; 35 | 36 | private float mFromDegrees; 37 | 38 | private float mToDegrees; 39 | 40 | private int mPivotXType = ABSOLUTE; 41 | 42 | private int mPivotYType = ABSOLUTE; 43 | 44 | private float mPivotXValue = 0.0f; 45 | 46 | private float mPivotYValue = 0.0f; 47 | 48 | private float mPivotX; 49 | 50 | private float mPivotY; 51 | 52 | /** 53 | * 位移动画的构造函数 54 | * @param fromXDelta 55 | * 动画开始时的X坐标 56 | * @param toXDelta 57 | * 动画结束时的X坐标 58 | * @param fromYDelta 59 | * 动画开始时的Y坐标 60 | * @param toYDelta 61 | * 动画结束时的Y坐标 62 | * 63 | * @param fromDegrees 64 | * 旋转开始时的角度 65 | * @param toDegrees 66 | * 旋转结束时的角度 67 | */ 68 | public RotateAndTranslateAnimation(float fromXDelta, float toXDelta, 69 | float fromYDelta, float toYDelta, float fromDegrees, float toDegrees) { 70 | mFromXValue = fromXDelta; 71 | mToXValue = toXDelta; 72 | mFromYValue = fromYDelta; 73 | mToYValue = toYDelta; 74 | 75 | mFromXType = ABSOLUTE; 76 | mToXType = ABSOLUTE; 77 | mFromYType = ABSOLUTE; 78 | mToYType = ABSOLUTE; 79 | 80 | mFromDegrees = fromDegrees; 81 | mToDegrees = toDegrees; 82 | 83 | mPivotXValue = 0.5f; 84 | mPivotXType = RELATIVE_TO_SELF; 85 | mPivotYValue = 0.5f; 86 | mPivotYType = RELATIVE_TO_SELF; 87 | } 88 | 89 | @Override 90 | protected void applyTransformation(float interpolatedTime, Transformation t) { 91 | final float degrees = mFromDegrees 92 | + ((mToDegrees - mFromDegrees) * interpolatedTime); 93 | if (mPivotX == 0.0f && mPivotY == 0.0f) { 94 | t.getMatrix().setRotate(degrees); 95 | } else { 96 | t.getMatrix().setRotate(degrees, mPivotX, mPivotY); 97 | } 98 | 99 | float dx = mFromXDelta; 100 | float dy = mFromYDelta; 101 | if (mFromXDelta != mToXDelta) { 102 | dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime); 103 | } 104 | if (mFromYDelta != mToYDelta) { 105 | dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime); 106 | } 107 | 108 | t.getMatrix().postTranslate(dx, dy); 109 | } 110 | 111 | @Override 112 | public void initialize(int width, int height, int parentWidth, 113 | int parentHeight) { 114 | super.initialize(width, height, parentWidth, parentHeight); 115 | mFromXDelta = resolveSize(mFromXType, mFromXValue, width, parentWidth); 116 | mToXDelta = resolveSize(mToXType, mToXValue, width, parentWidth); 117 | mFromYDelta = resolveSize(mFromYType, mFromYValue, height, parentHeight); 118 | mToYDelta = resolveSize(mToYType, mToYValue, height, parentHeight); 119 | 120 | mPivotX = resolveSize(mPivotXType, mPivotXValue, width, parentWidth); 121 | mPivotY = resolveSize(mPivotYType, mPivotYValue, height, parentHeight); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/composer_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/composer_button.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/composer_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/composer_camera.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/composer_icn_plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/composer_icn_plus.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/composer_music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/composer_music.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/composer_place.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/composer_place.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/composer_sleep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/composer_sleep.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/composer_sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/composer_sun.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/composer_thought.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/composer_thought.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/composer_with.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/composer_with.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/friends_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/friends_delete.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanOfDemo/FloatPathMenuSample/212c65608bb41c99e27b4cd5a331aaa571ef880c/floatpathmenu/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/drawable/button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /floatpathmenu/src/main/res/layout/activity_float.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 16 | 17 |