1. 背景
悬浮球是移动应用中常见的功能组件,特别是在游戏场景中,它能够在不占用太多屏幕空间的情况下提供快捷操作入口。本文将详细介绍如何在Android游戏中实现一个功能完善的悬浮球组件。
2.悬浮球具备的功能点
- 浮球吸附边缘后,2秒内无操作则自动隐藏一半
- 支持,上,下,左,右,四边缘吸附
- 触摸半隐藏的浮球时,会立即恢复完整显示并取消延迟任务
- 位置记忆功能(使用SharedPreferences持久化保存最后位置)
3. 核心代码实现
- 构造函数初始化
/**
* 构造函数
*
* @param context 应用上下文
*/
public FloatBallManager(Context context) {
// 使用弱引用存储Activity Context
mContextRef = new WeakReference<>(context);
Context cxt = mContextRef.get();
if (cxt == null) {
Log.e(TAG, "上下文为空,无法初始化悬浮球");
return;
}
this.ballSize = dip2px(50f);
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mSharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
mHandler = new Handler(context.getMainLooper());
mAutoHideRunnable = new AutoHideRunnable();
// 初始化屏幕尺寸
initScreenSize();
// 初始化悬浮球视图
initFloatBallView();
// 初始化布局参数
initLayoutParams();
// 从SharedPreferences恢复上次位置
restoreLastPosition();
}
- 初始化屏幕尺寸
/**
* 初始化屏幕尺寸信息
* 包括屏幕宽高和状态栏高度(用于调整Y轴位置计算)
*/
@SuppressLint("InternalInsetResource")
private void initScreenSize() {
Context context = getContext();
if (context == null) return;
// 获取屏幕尺寸
screenWidth = mWindowManager.getDefaultDisplay().getWidth();
screenHeight = mWindowManager.getDefaultDisplay().getHeight();
// 计算状态栏高度
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = getContext().getResources().getDimensionPixelSize(resourceId);
} else {
statusBarHeight = 0;
}
Log.d(TAG, "屏幕尺寸初始化: width=" + screenWidth + ", height=" + screenHeight + ", 状态栏高度=" + statusBarHeight);
}
- 初始化悬浮球视图
/**
* 初始化悬浮球视图
* 设置悬浮球图标、大小和触摸事件监听
*/
@SuppressLint("ClickableViewAccessibility")
private void initFloatBallView() {
Context context = getContext();
if (context == null) return;
mFloatBall = new ImageView(context);
// 设置悬浮球图标
mFloatBall.setImageResource(R.mipmap.ic_float_ball);
mFloatBall.setLayoutParams(new android.view.ViewGroup.LayoutParams(ballSize, ballSize));
// 设置悬浮球背景颜色
mFloatBall.setBackgroundColor(Color.parseColor("#ffffff"));
// 设置触摸事件监听
mFloatBall.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return handleTouchEvent(event);
}
});
}
- 初始化悬浮球布局参数
/**
* 初始化悬浮球布局参数
* 设置悬浮球类型、大小、透明度等窗口属性
*/
private void initLayoutParams() {
mLayoutParams = new WindowManager.LayoutParams();
// 设置窗口标记:透明、非聚焦、不接受输入等
mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
// 设置布局对齐方式为左上角
mLayoutParams.gravity = Gravity.TOP | Gravity.START;
// 设置悬浮球宽高
mLayoutParams.width = ballSize;
mLayoutParams.height = ballSize;
}
5.从SharedPreferences恢复上次位置
/**
* 从SharedPreferences恢复上次保存的位置
* 如果没有保存的位置,默认显示在屏幕右下角
*/
private void restoreLastPosition() {
lastSavedX = mSharedPreferences.getInt(KEY_LAST_X, screenWidth - ballSize);
lastSavedY = mSharedPreferences.getInt(KEY_LAST_Y, screenHeight / 2);
isHalfShow = mSharedPreferences.getBoolean(KEY_IS_HALF_SHOW, false);
mLayoutParams.x = lastSavedX;
mLayoutParams.y = lastSavedY - statusBarHeight; // 减去状态栏高度校正Y轴位置
// 如果上次是半显示状态,直接应用半显示位置
if (isHalfShow) {
applyHalfShowPosition();
}
Log.d(TAG, "恢复上次位置: x=" + lastSavedX + ", y=" + lastSavedY + ", 半显示状态=" + isHalfShow);
}
- 处理悬浮球触摸事件
/**
* 处理悬浮球触摸事件
* 支持拖动、点击和自动隐藏逻辑
*/
private boolean handleTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 触摸开始:取消自动隐藏任务,恢复完全显示状态
isDragging = false;
downX = event.getRawX();
downY = event.getRawY();
originalX = mLayoutParams.x;
originalY = mLayoutParams.y;
// 取消延迟半隐藏任务
mHandler.removeCallbacks(mAutoHideRunnable);
// 如果当前是半显示状态,恢复到完全显示位置
if (isHalfShow) {
restoreFullShowPosition();
}
return true;
case MotionEvent.ACTION_MOVE:
// 触摸移动:更新悬浮球位置,标记为拖动状态
float moveX = event.getRawX() - downX;
float moveY = event.getRawY() - downY;
// 如果移动距离超过阈值,视为拖动操作
if (Math.abs(moveX) > 5 || Math.abs(moveY) > 5) {
isDragging = true;
mLayoutParams.x = (int) (originalX + moveX);
mLayoutParams.y = (int) (originalY + moveY);
updateViewPosition(); // 更新悬浮球位置
}
return true;
case MotionEvent.ACTION_UP:
// 触摸结束:如果是拖动操作则吸附到最近边缘,启动自动隐藏任务
if (isDragging) {
// 吸附到最近边缘
attachToNearestEdge();
// 保存当前位置
saveCurrentPosition();
} else {
// 如果不是拖动操作,视为点击事件(可在此处添加点击处理逻辑)
handleBallClick();
}
// 无论是否拖动,都启动2秒后自动半隐藏任务
mHandler.postDelayed(mAutoHideRunnable, AUTO_HIDE_DELAY);
isDragging = false;
return true;
default:
return false;
}
}
- 吸附到最近的边缘
/**
* 吸附到最近的边缘
* 计算当前位置到上下左右四个边缘的距离,吸附到最近的边缘
*/
private void attachToNearestEdge() {
// 计算到各边缘的距离
int distanceToLeft = mLayoutParams.x;
int distanceToRight = screenWidth - (mLayoutParams.x + ballSize);
int distanceToTop = mLayoutParams.y;
int distanceToBottom = screenHeight - (mLayoutParams.y + ballSize);
// 找出最小距离
int minDistance = Math.min(Math.min(distanceToLeft, distanceToRight), Math.min(distanceToTop, distanceToBottom));
// 根据最小距离吸附到相应边缘
if (minDistance == distanceToLeft) {
mLayoutParams.x = 0; // 吸附到左边缘
} else if (minDistance == distanceToRight) {
mLayoutParams.x = screenWidth - ballSize; // 吸附到右边缘
} else if (minDistance == distanceToTop) {
mLayoutParams.y = 0; // 吸附到上边缘
} else {
mLayoutParams.y = screenHeight - ballSize; // 吸附到下边缘
}
updateViewPosition();
Log.d(TAG, "吸附到最近边缘: x=" + mLayoutParams.x + ", y=" + mLayoutParams.y);
}
- 应用半显示位置
/**
* 应用半显示位置
* 根据当前吸附的边缘,将悬浮球一半隐藏到屏幕外
*/
private void applyHalfShowPosition() {
// 根据当前位置判断吸附的边缘
if (mLayoutParams.x == 0) {
// 左边缘吸附:向左隐藏一半
mLayoutParams.x = -ballSize / 2;
} else if (mLayoutParams.x == screenWidth - ballSize) {
// 右边缘吸附:向右隐藏一半
mLayoutParams.x = screenWidth - ballSize / 2;
} else if (mLayoutParams.y == 0) {
// 上边缘吸附:向上隐藏一半
mLayoutParams.y = -ballSize / 2;
} else if (mLayoutParams.y == screenHeight - ballSize) {
// 下边缘吸附:向下隐藏一半
mLayoutParams.y = screenHeight - ballSize / 2;
}
isHalfShow = true;
updateViewPosition();
Log.d(TAG, "应用半显示位置: x=" + mLayoutParams.x + ", y=" + mLayoutParams.y);
}
- 恢复完全显示位置
/**
* 恢复完全显示位置
* 将半隐藏的悬浮球恢复到屏幕内完全显示
*/
private void restoreFullShowPosition() {
if (!isHalfShow) return;
// 根据当前半显示位置判断原吸附边缘
if (mLayoutParams.x == -ballSize / 2) {
mLayoutParams.x = 0; // 恢复左边缘完全显示
} else if (mLayoutParams.x == screenWidth - ballSize / 2) {
mLayoutParams.x = screenWidth - ballSize; // 恢复右边缘完全显示
} else if (mLayoutParams.y == -ballSize / 2) {
mLayoutParams.y = 0; // 恢复上边缘完全显示
} else if (mLayoutParams.y == screenHeight - ballSize / 2) {
mLayoutParams.y = screenHeight - ballSize; // 恢复下边缘完全显示
}
isHalfShow = false;
updateViewPosition();
Log.d(TAG, "恢复完全显示位置: x=" + mLayoutParams.x + ", y=" + mLayoutParams.y);
}
- 更新悬浮球视图位置
/**
* 更新悬浮球视图位置
* 调用WindowManager更新布局参数
*/
private void updateViewPosition() {
Context context = getContext();
// 新增:检查关键资源是否已释放
if (mWindowManager == null || mFloatBall == null || context == null) {
Log.w(TAG, "资源已释放,跳过视图更新");
return;
}
try {
if (mFloatBall.getParent() == null) {
mWindowManager.addView(mFloatBall, mLayoutParams);
} else {
mWindowManager.updateViewLayout(mFloatBall, mLayoutParams);
}
} catch (Exception e) {
Log.e(TAG, "更新悬浮球位置失败: " + e.getMessage());
}
}
- 保存当前位置到SharedPreferences
/**
* 保存当前位置到SharedPreferences
* 仅保存完全显示状态的位置,不保存半显示位置
*/
private void saveCurrentPosition() {
// 保存时校正Y轴位置(加上状态栏高度)
int actualY = mLayoutParams.y + statusBarHeight;
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putInt(KEY_LAST_X, mLayoutParams.x);
editor.putInt(KEY_LAST_Y, actualY);
editor.putBoolean(KEY_IS_HALF_SHOW, isHalfShow);
editor.apply();
lastSavedX = mLayoutParams.x;
lastSavedY = actualY;
Log.d(TAG, "保存当前位置: x=" + mLayoutParams.x + ", y=" + actualY + ", 半显示状态=" + isHalfShow);
}
- 显示悬浮球
/**
* 显示悬浮球
*/
public void showFloatBall() {
updateViewPosition();
// 显示后立即启动自动隐藏任务
mHandler.postDelayed(mAutoHideRunnable, AUTO_HIDE_DELAY);
Log.d(TAG, "显示悬浮球");
}
- 隐藏悬浮球
/**
* 隐藏悬浮球
*/
public void hideFloatBall() {
try {
// 1. 先彻底清除所有Handler任务,避免后续任务触发视图更新
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
// 2. 移除视图
if (mFloatBall != null && mFloatBall.getParent() != null && mWindowManager != null) {
mWindowManager.removeView(mFloatBall);
}
Log.d(TAG, "悬浮球已彻底移除");
} catch (Exception e) {
Log.e(TAG, "隐藏悬浮球失败: " + e.getMessage());
}
}
- 释放资源
/**
* 释放资源
*/
public void release() {
// 3. 释放所有引用(包括弱引用本身)
mContextRef.clear();
mContextRef = null;
mWindowManager = null;
mFloatBall = null;
mHandler = null;
Log.d(TAG, "资源已释放");
}
4. 完整代码
/**
* 悬浮球管理器核心类
* 功能特点:
* 1. 支持上下左右四边缘吸附
* 2. 位置记忆功能(使用SharedPreferences持久化保存最后位置)
* 3. 2秒无操作自动半隐藏(仅显示一半在屏幕内)
* 4. 触摸交互:拖动、点击事件处理
*/
public class FloatBallManager {
private static final String TAG = "FloatBallManager";
private static final String PREFS_NAME = "FloatBallPrefs";
private static final String KEY_LAST_X = "lastX";
private static final String KEY_LAST_Y = "lastY";
private static final String KEY_IS_HALF_SHOW = "isHalfShow";
// 自动半隐藏延迟时间(毫秒)
private static final long AUTO_HIDE_DELAY = 2000;
// 替换原有的mContext
private WeakReference<Context> mContextRef;
private WindowManager mWindowManager;
private WindowManager.LayoutParams mLayoutParams;
private ImageView mFloatBall;
private SharedPreferences mSharedPreferences;
// 屏幕和悬浮球尺寸相关变量
private int screenWidth;
private int screenHeight;
private int ballSize;
private int statusBarHeight;
private OnFloatBallClickListener mListener;
// 状态控制变量
private boolean isDragging = false;
private boolean isHalfShow = false;
private float downX, downY;
private float originalX, originalY;
private int lastSavedX, lastSavedY;
// 自动半隐藏相关
private AutoHideRunnable mAutoHideRunnable;
private Handler mHandler;
public interface OnFloatBallClickListener {
void onFloatBallClick();
}
/**
* 构造函数
*
* @param context 应用上下文
*/
public FloatBallManager(Context context) {
// 使用弱引用存储Activity Context
mContextRef = new WeakReference<>(context);
Context cxt = mContextRef.get();
if (cxt == null) {
Log.e(TAG, "上下文为空,无法初始化悬浮球");
return;
}
this.ballSize = dip2px(50f);
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mSharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
mHandler = new Handler(context.getMainLooper());
mAutoHideRunnable = new AutoHideRunnable();
// 初始化屏幕尺寸
initScreenSize();
// 初始化悬浮球视图
initFloatBallView();
// 初始化布局参数
initLayoutParams();
// 从SharedPreferences恢复上次位置
restoreLastPosition();
}
// 添加上下文获取工具方法
private Context getContext() {
return mContextRef != null ? mContextRef.get() : null;
}
/**
* 初始化屏幕尺寸信息
* 包括屏幕宽高和状态栏高度(用于调整Y轴位置计算)
*/
@SuppressLint("InternalInsetResource")
private void initScreenSize() {
Context context = getContext();
if (context == null) return;
// 获取屏幕尺寸
screenWidth = mWindowManager.getDefaultDisplay().getWidth();
screenHeight = mWindowManager.getDefaultDisplay().getHeight();
// 计算状态栏高度
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = getContext().getResources().getDimensionPixelSize(resourceId);
} else {
statusBarHeight = 0;
}
Log.d(TAG, "屏幕尺寸初始化: width=" + screenWidth + ", height=" + screenHeight + ", 状态栏高度=" + statusBarHeight);
}
/**
* 初始化悬浮球视图
* 设置悬浮球图标、大小和触摸事件监听
*/
@SuppressLint("ClickableViewAccessibility")
private void initFloatBallView() {
Context context = getContext();
if (context == null) return;
mFloatBall = new ImageView(context);
// 设置悬浮球图标
mFloatBall.setImageResource(R.mipmap.ic_float_ball);
mFloatBall.setLayoutParams(new android.view.ViewGroup.LayoutParams(ballSize, ballSize));
// 设置悬浮球背景颜色
mFloatBall.setBackgroundColor(Color.parseColor("#ffffff"));
// 设置触摸事件监听
mFloatBall.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return handleTouchEvent(event);
}
});
}
/**
* 初始化悬浮球布局参数
* 设置悬浮球类型、大小、透明度等窗口属性
*/
private void initLayoutParams() {
mLayoutParams = new WindowManager.LayoutParams();
// 设置窗口标记:透明、非聚焦、不接受输入等
mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
// 设置布局对齐方式为左上角
mLayoutParams.gravity = Gravity.TOP | Gravity.START;
// 设置悬浮球宽高
mLayoutParams.width = ballSize;
mLayoutParams.height = ballSize;
}
/**
* 从SharedPreferences恢复上次保存的位置
* 如果没有保存的位置,默认显示在屏幕右下角
*/
private void restoreLastPosition() {
lastSavedX = mSharedPreferences.getInt(KEY_LAST_X, screenWidth - ballSize);
lastSavedY = mSharedPreferences.getInt(KEY_LAST_Y, screenHeight / 2);
isHalfShow = mSharedPreferences.getBoolean(KEY_IS_HALF_SHOW, false);
mLayoutParams.x = lastSavedX;
mLayoutParams.y = lastSavedY - statusBarHeight; // 减去状态栏高度校正Y轴位置
// 如果上次是半显示状态,直接应用半显示位置
if (isHalfShow) {
applyHalfShowPosition();
}
Log.d(TAG, "恢复上次位置: x=" + lastSavedX + ", y=" + lastSavedY + ", 半显示状态=" + isHalfShow);
}
/**
* 处理悬浮球触摸事件
* 支持拖动、点击和自动隐藏逻辑
*/
private boolean handleTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 触摸开始:取消自动隐藏任务,恢复完全显示状态
isDragging = false;
downX = event.getRawX();
downY = event.getRawY();
originalX = mLayoutParams.x;
originalY = mLayoutParams.y;
// 取消延迟半隐藏任务
mHandler.removeCallbacks(mAutoHideRunnable);
// 如果当前是半显示状态,恢复到完全显示位置
if (isHalfShow) {
restoreFullShowPosition();
}
return true;
case MotionEvent.ACTION_MOVE:
// 触摸移动:更新悬浮球位置,标记为拖动状态
float moveX = event.getRawX() - downX;
float moveY = event.getRawY() - downY;
// 如果移动距离超过阈值,视为拖动操作
if (Math.abs(moveX) > 5 || Math.abs(moveY) > 5) {
isDragging = true;
mLayoutParams.x = (int) (originalX + moveX);
mLayoutParams.y = (int) (originalY + moveY);
updateViewPosition(); // 更新悬浮球位置
}
return true;
case MotionEvent.ACTION_UP:
// 触摸结束:如果是拖动操作则吸附到最近边缘,启动自动隐藏任务
if (isDragging) {
// 吸附到最近边缘
attachToNearestEdge();
// 保存当前位置
saveCurrentPosition();
} else {
// 如果不是拖动操作,视为点击事件(可在此处添加点击处理逻辑)
handleBallClick();
}
// 无论是否拖动,都启动2秒后自动半隐藏任务
mHandler.postDelayed(mAutoHideRunnable, AUTO_HIDE_DELAY);
isDragging = false;
return true;
default:
return false;
}
}
/**
* 吸附到最近的边缘
* 计算当前位置到上下左右四个边缘的距离,吸附到最近的边缘
*/
private void attachToNearestEdge() {
// 计算到各边缘的距离
int distanceToLeft = mLayoutParams.x;
int distanceToRight = screenWidth - (mLayoutParams.x + ballSize);
int distanceToTop = mLayoutParams.y;
int distanceToBottom = screenHeight - (mLayoutParams.y + ballSize);
// 找出最小距离
int minDistance = Math.min(Math.min(distanceToLeft, distanceToRight), Math.min(distanceToTop, distanceToBottom));
// 根据最小距离吸附到相应边缘
if (minDistance == distanceToLeft) {
mLayoutParams.x = 0; // 吸附到左边缘
} else if (minDistance == distanceToRight) {
mLayoutParams.x = screenWidth - ballSize; // 吸附到右边缘
} else if (minDistance == distanceToTop) {
mLayoutParams.y = 0; // 吸附到上边缘
} else {
mLayoutParams.y = screenHeight - ballSize; // 吸附到下边缘
}
updateViewPosition();
Log.d(TAG, "吸附到最近边缘: x=" + mLayoutParams.x + ", y=" + mLayoutParams.y);
}
/**
* 应用半显示位置
* 根据当前吸附的边缘,将悬浮球一半隐藏到屏幕外
*/
private void applyHalfShowPosition() {
// 根据当前位置判断吸附的边缘
if (mLayoutParams.x == 0) {
// 左边缘吸附:向左隐藏一半
mLayoutParams.x = -ballSize / 2;
} else if (mLayoutParams.x == screenWidth - ballSize) {
// 右边缘吸附:向右隐藏一半
mLayoutParams.x = screenWidth - ballSize / 2;
} else if (mLayoutParams.y == 0) {
// 上边缘吸附:向上隐藏一半
mLayoutParams.y = -ballSize / 2;
} else if (mLayoutParams.y == screenHeight - ballSize) {
// 下边缘吸附:向下隐藏一半
mLayoutParams.y = screenHeight - ballSize / 2;
}
isHalfShow = true;
updateViewPosition();
Log.d(TAG, "应用半显示位置: x=" + mLayoutParams.x + ", y=" + mLayoutParams.y);
}
/**
* 恢复完全显示位置
* 将半隐藏的悬浮球恢复到屏幕内完全显示
*/
private void restoreFullShowPosition() {
if (!isHalfShow) return;
// 根据当前半显示位置判断原吸附边缘
if (mLayoutParams.x == -ballSize / 2) {
mLayoutParams.x = 0; // 恢复左边缘完全显示
} else if (mLayoutParams.x == screenWidth - ballSize / 2) {
mLayoutParams.x = screenWidth - ballSize; // 恢复右边缘完全显示
} else if (mLayoutParams.y == -ballSize / 2) {
mLayoutParams.y = 0; // 恢复上边缘完全显示
} else if (mLayoutParams.y == screenHeight - ballSize / 2) {
mLayoutParams.y = screenHeight - ballSize; // 恢复下边缘完全显示
}
isHalfShow = false;
updateViewPosition();
Log.d(TAG, "恢复完全显示位置: x=" + mLayoutParams.x + ", y=" + mLayoutParams.y);
}
/**
* 更新悬浮球视图位置
* 调用WindowManager更新布局参数
*/
private void updateViewPosition() {
Context context = getContext();
// 新增:检查关键资源是否已释放
if (mWindowManager == null || mFloatBall == null || context == null) {
Log.w(TAG, "资源已释放,跳过视图更新");
return;
}
try {
if (mFloatBall.getParent() == null) {
mWindowManager.addView(mFloatBall, mLayoutParams);
} else {
mWindowManager.updateViewLayout(mFloatBall, mLayoutParams);
}
} catch (Exception e) {
Log.e(TAG, "更新悬浮球位置失败: " + e.getMessage());
}
}
/**
* 保存当前位置到SharedPreferences
* 仅保存完全显示状态的位置,不保存半显示位置
*/
private void saveCurrentPosition() {
// 保存时校正Y轴位置(加上状态栏高度)
int actualY = mLayoutParams.y + statusBarHeight;
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putInt(KEY_LAST_X, mLayoutParams.x);
editor.putInt(KEY_LAST_Y, actualY);
editor.putBoolean(KEY_IS_HALF_SHOW, isHalfShow);
editor.apply();
lastSavedX = mLayoutParams.x;
lastSavedY = actualY;
Log.d(TAG, "保存当前位置: x=" + mLayoutParams.x + ", y=" + actualY + ", 半显示状态=" + isHalfShow);
}
/**
* 处理悬浮球点击事件
* 可在此处添加悬浮球点击后的具体功能逻辑
*/
private void handleBallClick() {
if (mListener != null) {
mListener.onFloatBallClick();
}
Log.d(TAG, "悬浮球被点击");
}
/**
* 显示悬浮球
*/
public void showFloatBall() {
updateViewPosition();
// 显示后立即启动自动隐藏任务
mHandler.postDelayed(mAutoHideRunnable, AUTO_HIDE_DELAY);
Log.d(TAG, "显示悬浮球");
}
/**
* 隐藏悬浮球
*/
public void hideFloatBall() {
try {
// 1. 先彻底清除所有Handler任务,避免后续任务触发视图更新
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
// 2. 移除视图
if (mFloatBall != null && mFloatBall.getParent() != null && mWindowManager != null) {
mWindowManager.removeView(mFloatBall);
}
Log.d(TAG, "悬浮球已彻底移除");
} catch (Exception e) {
Log.e(TAG, "隐藏悬浮球失败: " + e.getMessage());
}
}
/**
* 释放资源
*/
public void release() {
// 3. 释放所有引用(包括弱引用本身)
mContextRef.clear();
mContextRef = null;
mWindowManager = null;
mFloatBall = null;
mHandler = null;
Log.d(TAG, "资源已释放");
}
public void setOnFloatBallClickListener(OnFloatBallClickListener listener) {
this.mListener = listener;
}
/**
* 自动半隐藏任务
* 实现2秒无操作后自动将悬浮球半隐藏到边缘
*/
private class AutoHideRunnable implements Runnable {
@Override
public void run() {
// 如果正在拖动或已经是半显示状态,不执行
if (isDragging || isHalfShow) {
return;
}
// 应用半显示位置
applyHalfShowPosition();
// 保存半显示状态(位置会在ACTION_UP时保存)
saveCurrentPosition();
}
}
private int dip2px(float dpValue) {
// dp转px单位
Context context = getContext();
if (context == null) {
return 0;
}
final float scale = getContext().getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}
5. 如何使用?
在Activity中使用FloatBallManager
public class MainActivity extends AppCompatActivity {
private FloatBallManager floatBallManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 设置全屏模式
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
// 创建浮球管理器
floatBallManager = new FloatBallManager(this);
// 设置点击监听(可选)
floatBallManager.setOnFloatBallClickListener(new FloatBallManager.OnFloatBallClickListener() {
@Override
public void onFloatBallClick() {
// 处理浮球点击事件
Toast.makeText(MainActivity.this, "浮球被点击~", Toast.LENGTH_SHORT).show();
}
});
}
@Override
protected void onResume() {
super.onResume();
// 显示浮球
floatBallManager.showFloatBall();
}
@Override
protected void onPause() {
super.onPause();
// 隐藏浮球
floatBallManager.hideFloatBall();
}
@Override
protected void onDestroy() {
// 隐藏悬浮球
if (floatBallManager != null) {
floatBallManager.release();
floatBallManager = null;
}
super.onDestroy();
}
}
注意事项:
- 避免在后台Activity显示悬浮窗 :只有当Activity处于前台时才调用 showFloatBall() ,可在
onResume()
中显示,onPause()
中隐藏。
6.运行效果截图
7. 关于作者其它项目视频教程介绍
本人在b站录制的一些视频教程项目,免费供大家学习
- Android新闻资讯app实战:https://www.bilibili.com/video/BV1CA1vYoEad/?vd_source=984bb03f768809c7d33f20179343d8c8
- Androidstudio开发购物商城实战:https://www.bilibili.com/video/BV1PjHfeXE8U/?vd_source=984bb03f768809c7d33f20179343d8c8
- Android开发备忘录记事本实战:https://www.bilibili.com/video/BV1FJ4m1u76G?vd_source=984bb03f768809c7d33f20179343d8c8&spm_id_from=333.788.videopod.sections
- Androidstudio底部导航栏实现:https://www.bilibili.com/video/BV1XB4y1d7et/?spm_id_from=333.337.search-card.all.click&vd_source=984bb03f768809c7d33f20179343d8c8
- Android使用TabLayout+ViewPager2实现左右滑动切换:https://www.bilibili.com/video/BV1Mz4y1c7eX/?spm_id_from=333.337.search-card.all.click&vd_source=984bb03f768809c7d33f20179343d8c8