├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── phone │ │ └── custom │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── phone │ │ │ └── custom │ │ │ ├── FloatViewService.java │ │ │ ├── MainActivity.java │ │ │ ├── MyWindowManager.java │ │ │ ├── TestActivity.java │ │ │ └── widget │ │ │ ├── Ball.java │ │ │ ├── FloatView.java │ │ │ ├── InnerBall.java │ │ │ ├── OutterBall.java │ │ │ └── WrapFloatView.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_test.xml │ │ └── layout_float.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 │ └── phone │ └── custom │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── preview ├── FloatView_00.gif ├── FloatView_01.gif ├── FloatView_desc_00.png ├── FloatView_desc_01.png └── FloatView_desc_02.png └── 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 | .externalNativeBuild 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/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 29 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 1.8 61 | 62 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FloatingView 2 | 3 | ### 响应操作有5种: 4 | * CLICK 点击操作 5 | * LEFT 向左拖拽 6 | * UP 向上拖拽 7 | * RIGHT 向右拖拽 8 | * DOWN 向下拖拽 9 | 10 | ### View布局结构 11 | 外面一个WarpFloatView 12 | 里面一个FloatView 13 | 14 | ![](preview/FloatView_desc_00.png) 15 | 16 | ### 事件处理 17 | * WrapFloatView处理长按、点击事件 18 | * FlatView处理move事件,并判断拖拽方向 19 | 20 | ### 绘制解析 21 | 22 | 该效果中有两个圆球,这里定义为Ball类,有两个继承类: 23 | * OutterBall 记录外面圆球参数 24 | * InnerBall 记录里面圆球参数 25 | 26 | **Ball**类负责根据当前圆球的参数绘制圆球。有如下参数: 27 | * mParentCenter FloatView的中心点 28 | * mCenter Ball的中心点 29 | * mRadius 垂直方向半径 30 | * mLeftRadius 左半径 31 | * mRightRadius 右半径 32 | * mAngle 绘制时,画板的旋转角度 33 | 34 | 示意图如下: 35 | 36 | ![](preview/FloatView_desc_01.png) 37 | ![](preview/FloatView_desc_02.png) 38 | 39 | 普通效果图: 40 | 41 | ![](preview/FloatView_00.gif) 42 | 43 | 悬浮效果图 44 | 45 | ![](preview/FloatView_01.gif) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | defaultConfig { 7 | applicationId "com.phone.custom" 8 | minSdkVersion 21 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.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 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:25.3.1' 28 | testCompile 'junit:junit:4.12' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\dell\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/phone/custom/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.phone.custom; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.phone.custom", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/phone/custom/FloatViewService.java: -------------------------------------------------------------------------------- 1 | package com.phone.custom; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.os.IBinder; 6 | 7 | /** 8 | * Created by Phone on 2017/5/26. 9 | */ 10 | 11 | public class FloatViewService extends Service { 12 | 13 | @Override 14 | public IBinder onBind(Intent intent) { 15 | return null; 16 | } 17 | 18 | @Override 19 | public void onCreate() { 20 | super.onCreate(); 21 | MyWindowManager.createFloatView(this); 22 | } 23 | 24 | @Override 25 | public int onStartCommand(Intent intent, int flags, int startId) { 26 | MyWindowManager.createFloatView(this); 27 | return super.onStartCommand(intent, flags, startId); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/phone/custom/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.phone.custom; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.widget.Button; 8 | 9 | public class MainActivity extends Activity { 10 | 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.activity_main); 15 | Button startFloatWindow = (Button) findViewById(R.id.start_float_window); 16 | startFloatWindow.setOnClickListener(new View.OnClickListener() { 17 | @Override 18 | public void onClick(View arg0) { 19 | Intent intent = new Intent(MainActivity.this, FloatViewService.class); 20 | startService(intent); 21 | finish(); 22 | } 23 | }); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/phone/custom/MyWindowManager.java: -------------------------------------------------------------------------------- 1 | package com.phone.custom; 2 | 3 | import android.content.Context; 4 | import android.graphics.PixelFormat; 5 | import android.view.Gravity; 6 | import android.view.WindowManager; 7 | import android.view.WindowManager.LayoutParams; 8 | 9 | import com.phone.custom.widget.WrapFloatView; 10 | 11 | /** 12 | * Created by Phone on 2017/5/26. 13 | */ 14 | 15 | public class MyWindowManager { 16 | 17 | private static WrapFloatView wrapFloatView; 18 | 19 | private static LayoutParams layoutParams; 20 | 21 | private static WindowManager mWindowManager; 22 | 23 | /** 24 | * 创建悬浮View 25 | * 26 | * @param context 27 | */ 28 | public static void createFloatView(Context context) { 29 | WindowManager windowManager = getWindowManager(context); 30 | int screenWidth = windowManager.getDefaultDisplay().getWidth(); 31 | int screenHeight = windowManager.getDefaultDisplay().getHeight(); 32 | if (wrapFloatView == null) { 33 | wrapFloatView = new WrapFloatView(context); 34 | if (layoutParams == null) { 35 | layoutParams = new LayoutParams(); 36 | layoutParams.type = LayoutParams.TYPE_PHONE; 37 | layoutParams.format = PixelFormat.RGBA_8888; 38 | layoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE; 39 | layoutParams.gravity = Gravity.LEFT | Gravity.TOP; 40 | layoutParams.width = 200; 41 | layoutParams.height = 200; 42 | layoutParams.x = screenWidth; 43 | layoutParams.y = screenHeight / 2; 44 | } 45 | wrapFloatView.setParams(layoutParams); 46 | windowManager.addView(wrapFloatView, layoutParams); 47 | } 48 | } 49 | 50 | /** 51 | * 移除悬浮View 52 | * 53 | * @param context 54 | */ 55 | public static void removeFloatView(Context context) { 56 | if (wrapFloatView != null) { 57 | WindowManager windowManager = getWindowManager(context); 58 | windowManager.removeView(wrapFloatView); 59 | wrapFloatView = null; 60 | } 61 | } 62 | 63 | /** 64 | * 悬浮View是否显示了 65 | * 66 | * @return 67 | */ 68 | public static boolean isWindowShowing() { 69 | return wrapFloatView != null; 70 | } 71 | 72 | private static WindowManager getWindowManager(Context context) { 73 | if (mWindowManager == null) { 74 | mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 75 | } 76 | return mWindowManager; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/phone/custom/TestActivity.java: -------------------------------------------------------------------------------- 1 | package com.phone.custom; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | /** 7 | * Created by Phone on 2017/5/26. 8 | */ 9 | 10 | public class TestActivity extends Activity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_test); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/phone/custom/widget/Ball.java: -------------------------------------------------------------------------------- 1 | package com.phone.custom.widget; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | import android.graphics.Point; 6 | import android.graphics.RadialGradient; 7 | import android.graphics.Shader; 8 | 9 | /** 10 | * Created by Phone on 2017/5/20. 11 | */ 12 | 13 | public abstract class Ball { 14 | 15 | private static final int mInnerColor = 0x00ffffff; 16 | private static final int mOutterColor = 0xaaffffff; 17 | //竖直方向半径 18 | protected int mRadius = 100; 19 | protected int mOriginRadius; 20 | //水平方向左半径 21 | protected int mLeftRadius = 100; 22 | //水平方向右半径 23 | protected int mRightRadius = 100; 24 | //绘制时画板需要旋转的角度 25 | protected float mAngle = 0; 26 | //椭圆的中心点 27 | protected Point mCenter; 28 | protected Point mOriginCenter; 29 | //颜色渐变的颜色值组 30 | protected int colors[] = { mInnerColor, mOutterColor }; 31 | //颜色渐变的颜色位置组 32 | protected float stops[] = { 0.5f, 1.0f }; 33 | //控件的中心点 34 | protected Point mParentCenter; 35 | //控件的半径 36 | protected int mParentRadius; 37 | //静止时椭圆大小与控件大小的比值 38 | protected float mSizeRate; 39 | protected Paint mPaint; 40 | private RadialGradient radialGradient; 41 | 42 | public Ball(int centerX, int centerY, int parentRadius, float sizeRate) { 43 | 44 | this.mParentCenter = new Point(centerX, centerY); 45 | this.mParentRadius = parentRadius; 46 | this.mSizeRate = sizeRate; 47 | init(); 48 | } 49 | 50 | private void init() { 51 | mCenter = new Point(mParentCenter.x, mParentCenter.y); 52 | mOriginCenter = new Point(mCenter.x, mCenter.y); 53 | mRadius = (int) (mParentRadius * mSizeRate); 54 | mOriginRadius = mRadius; 55 | mLeftRadius = mRadius; 56 | mRightRadius = mRadius; 57 | mAngle = 0; 58 | radialGradient = new RadialGradient(0, 0, mRadius, colors, stops, Shader.TileMode.CLAMP); 59 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 60 | mPaint.setStyle(Paint.Style.FILL); 61 | mPaint.setShader(radialGradient); 62 | } 63 | 64 | public void draw(Canvas canvas) { 65 | 66 | int x = mCenter.x - mParentCenter.x; 67 | int y = mCenter.y - mParentCenter.y; 68 | 69 | canvas.save(); 70 | canvas.translate(mParentCenter.x, mParentCenter.y); 71 | canvas.rotate(mAngle); 72 | canvas.translate(x, y); 73 | 74 | //绘制左半圆 75 | canvas.save(); 76 | canvas.scale(mLeftRadius * 1.0f / mOriginRadius, 1.0f); 77 | canvas.drawArc(-mLeftRadius, -mRadius, mLeftRadius, mRadius, 90, 180, false, mPaint); 78 | canvas.restore(); 79 | 80 | //绘制右半圆 81 | canvas.save(); 82 | canvas.scale(mRightRadius * 1.0f / mOriginRadius, 1.0f); 83 | canvas.drawArc(-mRightRadius, -mRadius, mRightRadius, mRadius, -90, 180, false, mPaint); 84 | canvas.restore(); 85 | 86 | canvas.restore(); 87 | } 88 | 89 | public void setAngle(float mAngle) { 90 | this.mAngle = mAngle; 91 | } 92 | 93 | public void setColorsAndPosition(int[] colors, float[] stops) { 94 | this.colors = colors; 95 | this.stops = stops; 96 | radialGradient = new RadialGradient(0, 0, mRadius, colors, stops, Shader.TileMode.CLAMP); 97 | mPaint.setShader(radialGradient); 98 | } 99 | 100 | public void setOffset(int offset) { 101 | float fraction = offset * 1.0f / mParentRadius; 102 | fraction = Math.min(fraction, 1.0f); 103 | calculateRadius(fraction); 104 | calculateLeftRadius(fraction); 105 | calculateRightRadius(fraction); 106 | calculateCenter(fraction); 107 | } 108 | 109 | public void setAllRadius(int radius) { 110 | mRadius = radius; 111 | mOriginRadius = radius; 112 | mLeftRadius = radius; 113 | mRightRadius = radius; 114 | } 115 | 116 | protected abstract void calculateCenter(float fraction); 117 | 118 | protected abstract void calculateRightRadius(float fraction); 119 | 120 | protected abstract void calculateLeftRadius(float fraction); 121 | 122 | protected abstract void calculateRadius(float fraction); 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/com/phone/custom/widget/FloatView.java: -------------------------------------------------------------------------------- 1 | package com.phone.custom.widget; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.content.Context; 7 | import android.graphics.Canvas; 8 | import android.graphics.Point; 9 | import android.util.AttributeSet; 10 | import android.util.Log; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | import android.view.ViewConfiguration; 14 | import android.view.animation.AccelerateDecelerateInterpolator; 15 | import android.view.animation.OvershootInterpolator; 16 | import android.widget.Toast; 17 | 18 | /** 19 | * Created by Phone on 2017/5/20. 20 | */ 21 | 22 | public class FloatView extends View { 23 | 24 | private static final int[] OUT_CIRCLE_COLOR_RANGE = { 0x00ffffff, 0x00ffffff, 0x1affffff, 0x62ffffff, 0x7effffff }; 25 | private static final float[] OUT_CIRCLE_POSITION_RANGE = { 0.0f, 0.45f, 0.6f, 0.9f, 1.0f }; 26 | private static final int[] INNER_CIRCLE_COLOR_RANGE = { 0xb2ffffff, 0xb2ffffff, 0xe5ffffff, 0xe5ffffff }; 27 | private static final float[] INNER_CIRCLE_POSITION_RANGE = { 0.0f, 0.05f, 0.75f, 1.0f }; 28 | 29 | /** 30 | * 触发操作的上限阈值 31 | */ 32 | private float mUpThreshold = 0.99f; 33 | /** 34 | * 触发操作的下限阈值 35 | */ 36 | private float mDownThreshold = 0.0f; 37 | /** 38 | * 大圆半径与FloatView半径的比例值 39 | */ 40 | private float mInnerRadiusRate = 0.4f; 41 | /** 42 | * 小圆半径与FloatView半径的比例值 43 | */ 44 | private float mOutterRadiusRate = 0.6f; 45 | /** 46 | * 轻微滑动阈值 47 | */ 48 | private int mScaleTouchSlop; 49 | /** 50 | * 是否正在拖拽 51 | */ 52 | private boolean isBeingDrag; 53 | /** 54 | * 外圆 55 | */ 56 | private OutterBall mOutterBall; 57 | /** 58 | * 内圆 59 | */ 60 | private InnerBall mInnerBall; 61 | /** 62 | * 中心点 63 | */ 64 | private Point center = new Point(); 65 | /** 66 | * 拖拽动画 67 | */ 68 | private ValueAnimator mDragAnimator; 69 | /** 70 | * 点击动画 71 | */ 72 | private ValueAnimator mClickAnimator; 73 | /** 74 | * 当前拖拽的距离 75 | */ 76 | private int mDistance; 77 | /** 78 | * down事件的横坐标 79 | */ 80 | private int mDownMotionX; 81 | /** 82 | * down事件的纵坐标 83 | */ 84 | private int mDownMotionY; 85 | /** 86 | * 当前手势 87 | */ 88 | private Mode mode = Mode.NONE; 89 | 90 | public enum Mode { 91 | CLICK, LEFT, UP, RIGHT, DOWN, NONE 92 | } 93 | 94 | public FloatView(Context context) { 95 | this(context, null); 96 | } 97 | 98 | public FloatView(Context context, AttributeSet attrs) { 99 | this(context, attrs, 0); 100 | } 101 | 102 | public FloatView(Context context, AttributeSet attrs, int defStyleAttr) { 103 | super(context, attrs, defStyleAttr); 104 | mScaleTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 105 | } 106 | 107 | @Override 108 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 109 | super.onSizeChanged(w, h, oldw, oldh); 110 | center.x = (getMeasuredWidth() + getPaddingLeft() - getPaddingRight()) / 2; 111 | center.y = (getMeasuredHeight() + getPaddingTop() - getPaddingBottom()) / 2; 112 | final int radius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 2; 113 | mInnerBall = new InnerBall(center.x, center.y, radius, mInnerRadiusRate); 114 | mInnerBall.setColorsAndPosition(INNER_CIRCLE_COLOR_RANGE, INNER_CIRCLE_POSITION_RANGE); 115 | mOutterBall = new OutterBall(center.x, center.y, radius, mOutterRadiusRate); 116 | mOutterBall.setColorsAndPosition(OUT_CIRCLE_COLOR_RANGE, OUT_CIRCLE_POSITION_RANGE); 117 | invalidate(); 118 | } 119 | 120 | @Override 121 | protected void onDraw(Canvas canvas) { 122 | super.onDraw(canvas); 123 | if (mInnerBall != null) { 124 | mInnerBall.draw(canvas); 125 | } 126 | if (mOutterBall != null) { 127 | mOutterBall.draw(canvas); 128 | } 129 | } 130 | 131 | @Override 132 | public boolean onTouchEvent(MotionEvent event) { 133 | 134 | if (isRunningOfAnimators()) { 135 | return false; 136 | } 137 | //在自定义处理事件前,先调用父类的处理方式 138 | boolean isConsume = super.onTouchEvent(event); 139 | switch (event.getAction()) { 140 | case MotionEvent.ACTION_DOWN: 141 | mDownMotionX = (int) event.getRawX(); 142 | mDownMotionY = (int) event.getRawY(); 143 | break; 144 | case MotionEvent.ACTION_MOVE: 145 | int distanceX = (int) (event.getRawX() - mDownMotionX); 146 | int distanceY = (int) (event.getRawY() - mDownMotionY); 147 | 148 | if (isBeingDrag || Math.abs(distanceX) > mScaleTouchSlop || Math.abs(distanceY) > mScaleTouchSlop) { 149 | isBeingDrag = true; 150 | 151 | mDistance = (int) Math.sqrt(distanceX * distanceX + distanceY * distanceY); 152 | 153 | float dx = event.getX() - center.x; 154 | float dy = event.getY() - center.y; 155 | 156 | float angle = calculateAngle(dx, dy); 157 | 158 | if (mOutterBall != null) { 159 | mOutterBall.setAngle(angle); 160 | mOutterBall.setOffset(mDistance); 161 | } 162 | if (mInnerBall != null) { 163 | mInnerBall.setAngle(angle); 164 | mInnerBall.setOffset(mDistance); 165 | } 166 | invalidate(); 167 | } 168 | break; 169 | case MotionEvent.ACTION_UP: 170 | isBeingDrag = false; 171 | float dx = event.getX() - center.x; 172 | float dy = event.getY() - center.y; 173 | startDragAnim(judgeWhichDirection(dx, dy)); 174 | break; 175 | case MotionEvent.ACTION_CANCEL: 176 | isBeingDrag = false; 177 | break; 178 | } 179 | return isConsume || event.getAction() == MotionEvent.ACTION_DOWN; 180 | } 181 | 182 | /** 183 | * 判定触发哪个方向的操作 184 | */ 185 | private Mode judgeWhichDirection(float dx, float dy) { 186 | Mode temp = Mode.NONE; 187 | double distance = Math.sqrt(dx * dx + dy * dy); 188 | int radius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 2; 189 | if (distance < radius * mUpThreshold || distance > radius * mDownThreshold) { 190 | if (Math.abs(dx) > Math.abs(dy)) { 191 | if (dx > 0) { 192 | temp = Mode.RIGHT; 193 | } else { 194 | temp = Mode.LEFT; 195 | } 196 | } else if (dy > 0) { 197 | temp = Mode.DOWN; 198 | } else { 199 | temp = Mode.UP; 200 | } 201 | } 202 | return temp; 203 | } 204 | 205 | private float calculateAngle(float dx, float dy) { 206 | if (dx == 0) { 207 | return 0; 208 | } 209 | float angle = (float) (Math.atan(dy / dx) * 180 / Math.PI); 210 | if (dx < 0) { 211 | angle += 180; 212 | } 213 | return angle; 214 | } 215 | 216 | /** 217 | * 开始拖拽动画 218 | * 219 | * @param newMode 220 | */ 221 | private void startDragAnim(final Mode newMode) { 222 | Log.i("phoneTest", "startDragAnim..." + newMode); 223 | //有动画正在执行,不响应 224 | if (isRunningOfAnimators()) { 225 | return; 226 | } 227 | mDragAnimator = ValueAnimator.ofInt(mDistance, 0); 228 | mDragAnimator.setIntValues(mDistance, 0); 229 | mDragAnimator.setDuration(400); 230 | mDragAnimator.setInterpolator(new OvershootInterpolator()); 231 | mDragAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 232 | @Override 233 | public void onAnimationUpdate(ValueAnimator valueAnimator) { 234 | mDistance = (int) valueAnimator.getAnimatedValue(); 235 | if (mOutterBall != null) { 236 | mOutterBall.setOffset(mDistance); 237 | } 238 | if (mInnerBall != null) { 239 | mInnerBall.setOffset(mDistance); 240 | } 241 | invalidate(); 242 | } 243 | }); 244 | mDragAnimator.addListener(new AnimatorListenerAdapter() { 245 | @Override 246 | public void onAnimationEnd(Animator animation) { 247 | super.onAnimationEnd(animation); 248 | //TODO 249 | doFunction(newMode); 250 | } 251 | }); 252 | mDragAnimator.start(); 253 | } 254 | 255 | /** 256 | * 执行点击动画 257 | */ 258 | public void startClickAnimator() { 259 | //有动画正在执行,不响应 260 | if (isRunningOfAnimators()) { 261 | return; 262 | } 263 | int radius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 2; 264 | mClickAnimator = ValueAnimator.ofFloat(radius * mInnerRadiusRate, radius * mInnerRadiusRate * 1.2f, 265 | radius * mInnerRadiusRate); 266 | if (mClickAnimator.isRunning()) { 267 | return; 268 | } 269 | mClickAnimator.setDuration(500); 270 | mClickAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 271 | mClickAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 272 | @Override 273 | public void onAnimationUpdate(ValueAnimator valueAnimator) { 274 | float value = (float) valueAnimator.getAnimatedValue(); 275 | if (mInnerBall != null) { 276 | mInnerBall.setAllRadius((int) value); 277 | } 278 | invalidate(); 279 | } 280 | }); 281 | mClickAnimator.addListener(new AnimatorListenerAdapter() { 282 | @Override 283 | public void onAnimationEnd(Animator animation) { 284 | super.onAnimationEnd(animation); 285 | //TODO 286 | doFunction(Mode.CLICK); 287 | } 288 | }); 289 | mClickAnimator.start(); 290 | } 291 | 292 | /** 293 | * 是否有动画正在执行 294 | * 295 | * @return 296 | */ 297 | private boolean isRunningOfAnimators() { 298 | if (mDragAnimator != null && mDragAnimator.isRunning()) { 299 | // Log.i("phoneTest", "drag animator is running"); 300 | return true; 301 | } 302 | if (mClickAnimator != null && mClickAnimator.isRunning()) { 303 | // Log.i("phoneTest", "click animator is running"); 304 | return true; 305 | } 306 | return false; 307 | } 308 | 309 | /** 310 | * 响应操作 311 | * 312 | * @param newMode 313 | */ 314 | private void doFunction(Mode newMode) { 315 | Log.i("phoneTest", "doFunction..." + newMode); 316 | if (newMode == mode) { 317 | return; 318 | } 319 | switch (newMode) { 320 | case CLICK: 321 | Log.i("phoneTest", "响应CLICK操作"); 322 | Toast.makeText(getContext(), "响应CLICK操作", Toast.LENGTH_SHORT).show(); 323 | break; 324 | case LEFT: 325 | Log.i("phoneTest", "响应LEFT操作"); 326 | Toast.makeText(getContext(), "响应LEFT操作", Toast.LENGTH_SHORT).show(); 327 | break; 328 | case UP: 329 | Log.i("phoneTest", "响应TOP操作"); 330 | Toast.makeText(getContext(), "响应TOP操作", Toast.LENGTH_SHORT).show(); 331 | break; 332 | case RIGHT: 333 | Log.i("phoneTest", "响应RIGHT操作"); 334 | Toast.makeText(getContext(), "响应RIGHT操作", Toast.LENGTH_SHORT).show(); 335 | break; 336 | case DOWN: 337 | Log.i("phoneTest", "响应BOTTOM操作"); 338 | Toast.makeText(getContext(), "响应BOTTOM操作", Toast.LENGTH_SHORT).show(); 339 | break; 340 | case NONE: 341 | Log.i("phoneTest", "响应NONE操作"); 342 | break; 343 | } 344 | mode = newMode; 345 | } 346 | 347 | } 348 | -------------------------------------------------------------------------------- /app/src/main/java/com/phone/custom/widget/InnerBall.java: -------------------------------------------------------------------------------- 1 | package com.phone.custom.widget; 2 | 3 | /** 4 | * Created by Phone on 2017/5/22. 5 | */ 6 | 7 | public class InnerBall extends Ball { 8 | 9 | public InnerBall(int centerX, int centerY, int parentRadius, float sizeRate) { 10 | super(centerX, centerY, parentRadius, sizeRate); 11 | } 12 | 13 | @Override 14 | protected void calculateCenter(float fraction) { 15 | float rate = fraction * 1.5f; 16 | mCenter.x = (int) (mParentCenter.x + mRadius * rate); 17 | } 18 | 19 | @Override 20 | protected void calculateRightRadius(float fraction) { 21 | // float rate = 1.0f + fraction * 0.1f; 22 | // mRightRadius = (int) (mOriginRadius * rate); 23 | } 24 | 25 | @Override 26 | protected void calculateLeftRadius(float fraction) { 27 | float rate = 1.0f + fraction * 0.1f; 28 | mLeftRadius = (int) (mOriginRadius * rate); 29 | } 30 | 31 | @Override 32 | protected void calculateRadius(float fraction) { 33 | float rate = 1.0f - fraction * 0.1f; 34 | mRadius = (int) (mOriginRadius * rate); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/phone/custom/widget/OutterBall.java: -------------------------------------------------------------------------------- 1 | package com.phone.custom.widget; 2 | 3 | /** 4 | * Created by Phone on 2017/5/22. 5 | */ 6 | 7 | public class OutterBall extends Ball { 8 | 9 | 10 | public OutterBall(int centerX, int centerY, int parentRadius, float sizeRate) { 11 | super(centerX, centerY, parentRadius, sizeRate); 12 | } 13 | 14 | @Override 15 | protected void calculateCenter(float fraction) { 16 | // float rate = fraction; 17 | // mCenter.x = (int) (mParentCenter.x + mRadius * rate); 18 | } 19 | 20 | @Override 21 | protected void calculateRightRadius(float fraction) { 22 | float rate = 1.0f + fraction * 0.05f; 23 | mRightRadius = (int) (mOriginRadius * rate); 24 | } 25 | 26 | @Override 27 | protected void calculateLeftRadius(float fraction) { 28 | float rate = 1.0f - fraction * 0.05f; 29 | mLeftRadius = (int) (mOriginRadius * rate); 30 | } 31 | 32 | @Override 33 | protected void calculateRadius(float fraction) { 34 | float rate = 1.0f - fraction * 0.1f; 35 | mRadius = (int) (mOriginRadius * rate); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/phone/custom/widget/WrapFloatView.java: -------------------------------------------------------------------------------- 1 | package com.phone.custom.widget; 2 | 3 | import android.content.Context; 4 | import android.os.Vibrator; 5 | import android.util.AttributeSet; 6 | import android.util.Log; 7 | import android.view.LayoutInflater; 8 | import android.view.MotionEvent; 9 | import android.view.View; 10 | import android.view.ViewConfiguration; 11 | import android.view.WindowManager; 12 | import android.widget.FrameLayout; 13 | 14 | import com.phone.custom.R; 15 | 16 | import java.lang.reflect.Field; 17 | 18 | /** 19 | * Created by Phone on 2017/5/26. 20 | */ 21 | 22 | public class WrapFloatView extends FrameLayout { 23 | 24 | /** 25 | * 记录悬浮View的宽度 26 | */ 27 | public static int viewWidth; 28 | 29 | /** 30 | * 记录悬浮View的高度 31 | */ 32 | public static int viewHeight; 33 | 34 | /** 35 | * 记录系统状态栏的高度 36 | */ 37 | private static int statusBarHeight; 38 | 39 | /** 40 | * 用于更新悬浮View的位置 41 | */ 42 | private WindowManager windowManager; 43 | 44 | /** 45 | * 悬浮View的布局参数 46 | */ 47 | private WindowManager.LayoutParams mParams; 48 | 49 | /** 50 | * 记录当前手指位置在屏幕上的横坐标值 51 | */ 52 | private float xInScreen; 53 | 54 | /** 55 | * 记录当前手指位置在屏幕上的纵坐标值 56 | */ 57 | private float yInScreen; 58 | 59 | /** 60 | * 记录手指按下时在悬浮View的上的横坐标的值 61 | */ 62 | private float xInView; 63 | 64 | /** 65 | * 记录手指按下时在悬浮View的上的纵坐标的值 66 | */ 67 | private float yInView; 68 | 69 | /** 70 | * 判定轻微滑动的阈值 71 | */ 72 | private int mScaleTouchSlop; 73 | /** 74 | * 上一次Down事件触发的时间点 75 | */ 76 | private long mLastDownTime; 77 | /** 78 | * 判定为长按的时间阈值 79 | */ 80 | private final static long LONG_CLICK_LIMIT = 300; 81 | 82 | private float mMotionDownX; 83 | private float mMotionDownY; 84 | /** 85 | * 从一次down到up/cancel之间所有move事件中,触发点与down事件的触发点的最大偏移量(取横、纵坐标的较大值) 86 | */ 87 | private float mMaxMoveDistance; 88 | 89 | /** 90 | * 震动服务 91 | */ 92 | private Vibrator mVibrator; 93 | private long[] mPattern = { 0, 100 }; 94 | 95 | public WrapFloatView(Context context) { 96 | this(context, null); 97 | } 98 | 99 | public WrapFloatView(Context context, AttributeSet attrs) { 100 | this(context, attrs, 0); 101 | } 102 | 103 | public WrapFloatView(Context context, AttributeSet attrs, int defStyleAttr) { 104 | super(context, attrs, defStyleAttr); 105 | mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); 106 | mScaleTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 107 | windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 108 | LayoutInflater.from(context).inflate(R.layout.layout_float, this); 109 | View view = findViewById(R.id.floatView); 110 | viewWidth = view.getLayoutParams().width; 111 | viewHeight = view.getLayoutParams().height; 112 | } 113 | 114 | @Override 115 | public boolean onInterceptTouchEvent(MotionEvent event) { 116 | switch (event.getActionMasked()) { 117 | case MotionEvent.ACTION_DOWN: 118 | //记录down事件触发的时间点 119 | mLastDownTime = System.currentTimeMillis(); 120 | mMotionDownX = event.getX(); 121 | mMotionDownY = event.getY(); 122 | // 手指按下时记录必要数据,纵坐标的值都需要减去状态栏高度 123 | xInView = event.getX(); 124 | yInView = event.getY(); 125 | xInScreen = event.getRawX(); 126 | yInScreen = event.getRawY() - getStatusBarHeight(); 127 | break; 128 | case MotionEvent.ACTION_MOVE: 129 | getMaxMoveDistance(event); 130 | if (isLongTouch()) { 131 | mVibrator.vibrate(mPattern, -1); 132 | doLongPressEffect(); 133 | return true; 134 | } 135 | break; 136 | case MotionEvent.ACTION_UP: 137 | case MotionEvent.ACTION_CANCEL: 138 | if (isClick(event)) { 139 | doClickEffect(); 140 | } 141 | resetMaxMoveDistance(); 142 | break; 143 | } 144 | return super.onInterceptTouchEvent(event); 145 | } 146 | 147 | @Override 148 | public boolean onTouchEvent(MotionEvent event) { 149 | switch (event.getAction()) { 150 | case MotionEvent.ACTION_DOWN: 151 | // 手指按下时记录必要数据,纵坐标的值都需要减去状态栏高度 152 | xInView = event.getX(); 153 | yInView = event.getY(); 154 | xInScreen = event.getRawX(); 155 | yInScreen = event.getRawY() - getStatusBarHeight(); 156 | break; 157 | case MotionEvent.ACTION_MOVE: 158 | getMaxMoveDistance(event); 159 | xInScreen = event.getRawX(); 160 | yInScreen = event.getRawY() - getStatusBarHeight(); 161 | // 手指移动的时候更新悬浮控件的位置 162 | updateViewPosition(); 163 | break; 164 | case MotionEvent.ACTION_UP: 165 | case MotionEvent.ACTION_CANCEL: 166 | resetMaxMoveDistance(); 167 | break; 168 | default: 169 | break; 170 | } 171 | return true; 172 | } 173 | 174 | /** 175 | * 将悬浮View的参数传入,用于更新小悬浮窗的位置。 176 | * 177 | * @param params 小悬浮窗的参数 178 | */ 179 | public void setParams(WindowManager.LayoutParams params) { 180 | mParams = params; 181 | } 182 | 183 | /** 184 | * 更新悬浮View在屏幕中的位置。 185 | */ 186 | private void updateViewPosition() { 187 | Log.i("phoneTest", "xInView:" + xInView + ", yInView:" + yInView); 188 | mParams.x = (int) (xInScreen - xInView); 189 | mParams.y = (int) (yInScreen - yInView); 190 | windowManager.updateViewLayout(this, mParams); 191 | } 192 | 193 | /** 194 | * 用于获取状态栏的高度。 195 | * 196 | * @return 返回状态栏高度的像素值。 197 | */ 198 | private int getStatusBarHeight() { 199 | if (statusBarHeight == 0) { 200 | try { 201 | Class c = Class.forName("com.android.internal.R$dimen"); 202 | Object o = c.newInstance(); 203 | Field field = c.getField("status_bar_height"); 204 | int x = (Integer) field.get(o); 205 | statusBarHeight = getResources().getDimensionPixelSize(x); 206 | } catch (Exception e) { 207 | e.printStackTrace(); 208 | } 209 | } 210 | return statusBarHeight; 211 | } 212 | 213 | /** 214 | * 是否是长按 215 | * 216 | * @return 217 | */ 218 | private boolean isLongTouch() { 219 | long time = System.currentTimeMillis(); 220 | //需要满足两个条件:1.所有move事件与down事件之间的偏移量小于阈值,2.该move事件与down之间之间的时间差大于阈值 221 | if (mMaxMoveDistance < mScaleTouchSlop && (time - mLastDownTime >= LONG_CLICK_LIMIT)) { 222 | return true; 223 | } 224 | return false; 225 | } 226 | 227 | /** 228 | * 是否是点击 229 | * 230 | * @param event 231 | * @return 232 | */ 233 | private boolean isClick(MotionEvent event) { 234 | //满足一个条件:1.所有move事件与down事件之间的偏移量小于阈值 235 | if (mMaxMoveDistance < mScaleTouchSlop) { 236 | return true; 237 | } 238 | return false; 239 | } 240 | 241 | /** 242 | * 所有move事件与down事件的坐标偏移量最大值 243 | * 244 | * @param event 245 | */ 246 | private void getMaxMoveDistance(MotionEvent event) { 247 | float dx = Math.abs(event.getX() - mMotionDownX); 248 | float dy = Math.abs(event.getY() - mMotionDownY); 249 | float maxValue = Math.max(dx, dy); 250 | mMaxMoveDistance = Math.max(mMaxMoveDistance, maxValue); 251 | } 252 | 253 | private void resetMaxMoveDistance() { 254 | mMaxMoveDistance = 0; 255 | } 256 | 257 | /** 258 | * 执行点击效果 259 | */ 260 | private void doClickEffect() { 261 | final FloatView child = (FloatView) findViewById(R.id.floatView); 262 | child.startClickAnimator(); 263 | } 264 | 265 | /** 266 | * 执行长按效果 267 | */ 268 | private void doLongPressEffect() { 269 | final FloatView child = (FloatView) findViewById(R.id.floatView); 270 | // child.startLongPressAnim(); 271 | } 272 | 273 | } 274 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |