前言:
实现这个功能,可能需要你对WindowManager有一定认识,大家可以自行去看大佬们关于WindowManager的文章。需要基本的自定义View相关的知识以及onTouch相关参数的理解。
开始操作:
既然是用WindowManager,当然开始是初始化咯:
这里比较重要是用WindowManager的add方法,添加了一个View在最顶层,这样这个View就类似悬浮在页面之上了。
/**
* 初始化 参数
*/
private void init(){
STATE = STATE_NOMOR;
mWindowManager = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
mParams = new LayoutParams(100,100,0,0, PixelFormat.TRANSPARENT);
mParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mParams.width = 100;
mParams.height = 100;
mParams.gravity = Gravity.LEFT | Gravity.TOP;
mParams.x = 0;
mParams.y = 300;
mWindowManager.addView(this , mParams);
screenWidth = mWindowManager.getDefaultDisplay().getWidth();
screenHeight = mWindowManager.getDefaultDisplay().getHeight();
}
1.自定义View绘制出模样
我们新建一个文件取名FloatingView,继承自Button,为什么不继承View,因为想着以后可以想拓展可以省很多事。具体要注意的就是自定义View的几个重要的方法需要重写:onMeasure(测量)、onLayout(布局,这个控件没用到)、onDraw(绘制):
onMeasure:这个方法里面经过测量后,你能得到测量后控件的宽高位置坐标等数据。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getWidth();
height = getHeight();
x = getX();
y = getY();
initNumber();
}
onDraw:绘制不通状态下的控件形态(open状态目前还没开发)
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
switch (STATE){
case STATE_MOVE:
changeState2_MOVE();
drawD(canvas);
break;
case STATE_NOMOR:
changeState2_NOMOR();
drawD(canvas);
break;
case STATE_OPEN:
changeSate2_OPEN();
drawOpen(canvas);
break;
default:
break;
}
}
这里只截取了部分方法,整个类的代码在文末给出。
前面这部分整理完基本就可以看到效果如下图:
2.实现可以拖拽效果
在要使用的Activity初始化FloatingView的一些数据,为了方便,我把大部分初始化操作都放在FloatingView中,在onTouch中手指放下、移动、抬起时处理FloatingView相关的状态:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_register);
mButton = new FloatingView(this);
mButton.setOnTouchListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
float rawX = event.getRawX();
float rawY = event.getRawY();
int action = event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
mButton.setState(FloatingView.STATE_MOVE);
break;
case MotionEvent.ACTION_MOVE:
mButton.onActionMove((int)rawX , (int)rawY);
break;
case MotionEvent.ACTION_UP:
mButton.onActionUp(rawX , rawY);
break;
default:
break;
}
return false;
}
想要实现拖拽,最重要的就是WindowManager中的updateViewLayout()方法,调用这个方法,把当前坐标的信息给参数中的LayoutParams,就可以刷新位置了,实现了拖拽的效果,并且是在页面之上,悬浮拖动的效果就大致出来了。但是ios那个悬浮的框框还有个功能就是,不会悬浮在屏幕中间,会贴边显示,下面我们就来分析一下这个过程。
2.onTouch计算使其贴边,并且让其在获取到焦点时改变透明度
思路:我们把屏幕分成四等份如下图:
如图所示当前悬浮按钮是在左上的区域,但是如果手指滑动让其在离左边框的距离大于上边框的距离时松开手指,此时它应该自动贴边到上边框,反之就贴边左边框,其他四个区域也是类似的思路,但是在计算距离时要注意一些细节;
计算左上、右下时,可以直接使用rawX,rawY(与x,y坐标性质不同,xy是相对于父控件的位置,前者是相对于屏幕的坐标位置)和屏幕宽高比较。但是在计算左下和右上的时候,要使用rawX,rawY以及屏幕宽高混合计算。
核心算法在FloatingView中的cal方法中,我们通过计算它手指抬起时控件所属区域,并且计算应该贴在哪个边框,最终把计算控件应该贴边的位置坐标设置给LayoutParams的x,y中,调用WindowManager的updateViewLayout方法更新位置信息(为了方便我使用的是宽高比来算,这样其实不精准的):
/**
* 计算手指停留的位置,让控件贴边
* @param rawX 手指停留位置相对于屏幕的x轴坐标
* @param rawY 手指停留位置相对于屏幕的y轴坐标
*/
public void cal(float rawX , float rawY){
//左上 && 右下(因为两者比例都是大于1/2所以可以直接用比例来算)
float X = rawX/screenWidth;
float Y = rawY/screenHeight;
//右上
float R = screenWidth - rawX;
//左下
float B = screenHeight - rawY;
if(X <= 0.5 && Y <= 0.5){//左上
if(rawX - rawY >0){
setParamsXY((int)rawX , 0);
}else{
setParamsXY(0 , (int)rawY);
}
}else if(X > 0.5 && Y > 0.5){//右下
if(X - Y > 0){
setParamsXY(screenWidth , (int)rawY);
}else{
setParamsXY((int)rawX , screenHeight);
}
}else if(X > 0.5 && Y <=0.5){//右上
if(R - rawY > 0){
setParamsXY((int)rawX , 0);
}else{
setParamsXY(screenWidth , (int)rawY);
}
}else{//左下
if(rawX - B > 0){
setParamsXY((int)rawX , screenHeight);
}else{
setParamsXY(0 , (int)rawY);
}
}
}
其中setParamsXY方法就是设置LayoutParams的x,y,的值,让其强行改变控件位置达到贴边的效果:
/**
* 设置控件相对于屏幕位置坐标
* @param x x轴坐标
* @param y y轴坐标
*/
public void setParamsXY(int x,int y){
mParams.x = x;
mParams.y = y;
}
最终效果如下GIF所示:
对了还有就是在移动和手指落在控件上,会有一个背景透明度降低的过程,这样使控件看起来更亮。
在onTouch方法中设置它的不同状态即可,源码里面有这里不写了!
FloatingView的源码:
package com.xxx.your-packge-name.ui.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v7.widget.AppCompatButton;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
/**
* Created by:[ Jovial 比较喜欢丶笑]
* Created date:[ 2018/1/22 0022] 博客地址:http://my.csdn.net/caihongdao123?locationNum=0&fps=1
* About Class:[ 制作一个类似ios可以左右悬浮的球 ]
*/
public class FloatingView extends AppCompatButton {
/** 画笔 */
private Paint mPaint , mPaint1 , mPaint2 , mRectPaint ;
/** 控件 宽高 中心同心圆是根据宽高比例算出来的半径 */
private int width , height;
/** 控件的初始位置坐标 */
private float x , y ;
/** 同心圆 圆心位置 */
private float rx , ry;
/** 同心圆 半径 */
private int c1,c2,c3;
/** 控件的状态 移动&焦点(0) 静止状态(-1) 点击打开状态(2) */
public final static int STATE_MOVE = 0;
public final static int STATE_NOMOR = -1;
public final static int STATE_OPEN = 2;
private static int STATE = STATE_NOMOR;
/** 屏幕宽高 */
private int screenWidth;
private int screenHeight;
private WindowManager mWindowManager;
/** 悬浮按钮 */
private LayoutParams mParams;
/** 打开状态的界面布局 */
private LayoutParams mParamsOpen;
public FloatingView(Context context){
this(context, null , 0);
}
public FloatingView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs , 0);
}
public FloatingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// TypedArray a = context.obtainStyledAttributes(attrs , R.styleable.CircleView);
// mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
// a.recycle();
init();
initPaint();
}
/**
* 初始化 参数
*/
private void init(){
STATE = STATE_NOMOR;
mWindowManager = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
mParams = new LayoutParams(100,100,0,0, PixelFormat.TRANSPARENT);
mParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mParams.width = 100;
mParams.height = 100;
mParams.gravity = Gravity.LEFT | Gravity.TOP;
mParams.x = 0;
mParams.y = 300;
mWindowManager.addView(this , mParams);
screenWidth = mWindowManager.getDefaultDisplay().getWidth();
screenHeight = mWindowManager.getDefaultDisplay().getHeight();
}
/**
* 初始化状态
*/
public void setState(int state){
STATE = state;
}
/**
* 状态类型:(-1)
* 控件透明度增加,属于无任何状态的情况
*/
public void changeState2_NOMOR(){
mPaint.setAlpha(70);
mPaint1.setAlpha(50);
mPaint2.setAlpha(40);
mRectPaint.setAlpha(60);
invalidate();
}
/**
* 状态类型(1)
* 移动或者获取到焦点,透明度降低,是控件看起来颜色更亮
*/
public void changeState2_MOVE(){
mPaint.setAlpha(90);
mPaint1.setAlpha(70);
mPaint2.setAlpha(60);
mRectPaint.setAlpha(100);
invalidate();
}
/**
* 类型状态(2)
* 当点击打开时,调用此方法重新绘制控件
*/
private void changeSate2_OPEN(){
}
/**
* 初始化画笔
*/
public void initPaint(){
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setAlpha(70);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint1 = new Paint();
mPaint1.setColor(Color.WHITE);
mPaint1.setStrokeCap(Paint.Cap.ROUND);
mPaint1.setAlpha(50);
mPaint1.setAntiAlias(true);
mPaint1.setStyle(Paint.Style.FILL);
mPaint2 = new Paint();
mPaint2.setColor(Color.WHITE);
mPaint2.setStrokeCap(Paint.Cap.ROUND);
mPaint2.setAlpha(40);
mPaint2.setAntiAlias(true);
mPaint2.setStyle(Paint.Style.FILL);
mRectPaint = new Paint();
mRectPaint.setColor(Color.BLACK);
mRectPaint.setStrokeCap(Paint.Cap.ROUND);
mRectPaint.setAntiAlias(true);
mRectPaint.setAlpha(60);
mRectPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getWidth();
height = getHeight();
x = getX();
y = getY();
initNumber();
}
/**
* 计算控件绘制的参数
*/
public void initNumber(){
rx = x + width/2;
ry = y + height/2;
c1 = width/5+14;
c2 = width/5+8;
c3 = width/5;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
switch (STATE){
case STATE_MOVE:
changeState2_MOVE();
drawD(canvas);
break;
case STATE_NOMOR:
changeState2_NOMOR();
drawD(canvas);
break;
case STATE_OPEN:
changeSate2_OPEN();
drawOpen(canvas);
break;
default:
break;
}
}
/**
* 绘制未点击状态的控件形态
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void drawD(Canvas canvas){
canvas.drawRoundRect(x,y,width,height,20f,20f,mRectPaint);
canvas.drawCircle(rx,ry,c1,mPaint1);
canvas.drawCircle(rx,ry,c2,mPaint2);
canvas.drawCircle(rx,ry,c3,mPaint);
}
/**
* 绘制点击打开状态(2)的效果
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void drawOpen(Canvas canvas){
canvas.drawRoundRect(x,y,screenWidth/2,screenHeight/2,20f,20f,mRectPaint);
}
/**
* 计算手指停留的位置,让控件贴边
* @param rawX 手指停留位置相对于屏幕的x轴坐标
* @param rawY 手指停留位置相对于屏幕的y轴坐标
*/
public void cal(float rawX , float rawY){
//左上 && 右下(因为两者比例都是大于1/2所以可以直接用比例来算)
float X = rawX/screenWidth;
float Y = rawY/screenHeight;
//右上
float R = screenWidth - rawX;
//左下
float B = screenHeight - rawY;
if(X <= 0.5 && Y <= 0.5){//左上
if(rawX - rawY >0){
setParamsXY((int)rawX , 0);
}else{
setParamsXY(0 , (int)rawY);
}
}else if(X > 0.5 && Y > 0.5){//右下
if(X - Y > 0){
setParamsXY(screenWidth , (int)rawY);
}else{
setParamsXY((int)rawX , screenHeight);
}
}else if(X > 0.5 && Y <=0.5){//右上
if(R - rawY > 0){
setParamsXY((int)rawX , 0);
}else{
setParamsXY(screenWidth , (int)rawY);
}
}else{//左下
if(rawX - B > 0){
setParamsXY((int)rawX , screenHeight);
}else{
setParamsXY(0 , (int)rawY);
}
}
}
/**
* 设置控件相对于屏幕位置坐标
* @param x x轴坐标
* @param y y轴坐标
*/
public void setParamsXY(int x,int y){
mParams.x = x;
mParams.y = y;
}
/**
* 更新控件位置,此处调用WindowManager的方法来更新
*/
public void updateViewLayout(){
mWindowManager.updateViewLayout(this , mParams);
}
/**
* 添加onTouch监听后,手指抬起时需要调用的方法
*/
public void onActionUp(float rawX , float rawY){
setState(FloatingView.STATE_NOMOR);
//计算控件距离上左下右边框的距离,让其贴边
cal(rawX , rawY);
updateViewLayout();
}
/**
* 添加onTouch监听后,手指移动时需要调用的方法
*/
public void onActionMove(float rawX , float rawY){
setParamsXY((int)rawX , (int)rawY);
updateViewLayout();
}
}
activity调用过程:
public class RegistActivity extends FragmentActivity implements View.OnTouchListener{
private FloatingView mButton;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_register);
mButton = new FloatingView(this);
mButton.setOnTouchListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
float rawX = event.getRawX();
float rawY = event.getRawY();
int action = event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
mButton.setState(FloatingView.STATE_MOVE);
break;
case MotionEvent.ACTION_MOVE:
mButton.onActionMove((int)rawX , (int)rawY);
break;
case MotionEvent.ACTION_UP:
mButton.onActionUp(rawX , rawY);
break;
default:
break;
}
return false;
}
}
代码已提交到git上,可直接下载运行,欢迎start!-->点击打开git
-----------------------------------分割线-------------------------------------------------------
添加了open状态:
说明:
采用的是一个dialog弹窗的方式,比较简单,不过里面放置一个可以让空间以圆环形势排列的自定义CircleLayout,该自定义控件是继承自LinearLayout,具体代码已提交git上。