Android开发,游戏内悬浮球实现

1. 背景

悬浮球是移动应用中常见的功能组件,特别是在游戏场景中,它能够在不占用太多屏幕空间的情况下提供快捷操作入口。本文将详细介绍如何在Android游戏中实现一个功能完善的悬浮球组件。

2.悬浮球具备的功能点

  • 浮球吸附边缘后,2秒内无操作则自动隐藏一半
  • 支持,上,下,左,右,四边缘吸附
  • 触摸半隐藏的浮球时,会立即恢复完整显示并取消延迟任务
  • 位置记忆功能(使用SharedPreferences持久化保存最后位置)

3. 核心代码实现

  1. 构造函数初始化
    /**
     * 构造函数
     *
     * @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();
    }
  1. 初始化屏幕尺寸
    /**
     * 初始化屏幕尺寸信息
     * 包括屏幕宽高和状态栏高度(用于调整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);
    }
  1. 初始化悬浮球视图
    /**
     * 初始化悬浮球视图
     * 设置悬浮球图标、大小和触摸事件监听
     */
    @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);
            }
        });
    }
  1. 初始化悬浮球布局参数
    /**
     * 初始化悬浮球布局参数
     * 设置悬浮球类型、大小、透明度等窗口属性
     */
    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);
    }
  1. 处理悬浮球触摸事件
      /**
     * 处理悬浮球触摸事件
     * 支持拖动、点击和自动隐藏逻辑
     */
    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;
        }
    }
  1. 吸附到最近的边缘
    /**
     * 吸附到最近的边缘
     * 计算当前位置到上下左右四个边缘的距离,吸附到最近的边缘
     */
    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);
    }
  1. 应用半显示位置
    /**
     * 应用半显示位置
     * 根据当前吸附的边缘,将悬浮球一半隐藏到屏幕外
     */
    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);
    }
  1. 恢复完全显示位置
     /**
     * 恢复完全显示位置
     * 将半隐藏的悬浮球恢复到屏幕内完全显示
     */
    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);
    }

  1. 更新悬浮球视图位置
    /**
     * 更新悬浮球视图位置
     * 调用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());
        }
    }
  1. 保存当前位置到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);
    }
  1. 显示悬浮球
    /**
     * 显示悬浮球
     */
    public void showFloatBall() {
        updateViewPosition();
        // 显示后立即启动自动隐藏任务
        mHandler.postDelayed(mAutoHideRunnable, AUTO_HIDE_DELAY);
        Log.d(TAG, "显示悬浮球");
    }

  1. 隐藏悬浮球
    /**
     * 隐藏悬浮球
     */
    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());
        }
    }
  1. 释放资源
    /**
     * 释放资源
     */

    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站录制的一些视频教程项目,免费供大家学习

  1. Android新闻资讯app实战:https://www.bilibili.com/video/BV1CA1vYoEad/?vd_source=984bb03f768809c7d33f20179343d8c8
  2. Androidstudio开发购物商城实战:https://www.bilibili.com/video/BV1PjHfeXE8U/?vd_source=984bb03f768809c7d33f20179343d8c8
  3. Android开发备忘录记事本实战:https://www.bilibili.com/video/BV1FJ4m1u76G?vd_source=984bb03f768809c7d33f20179343d8c8&spm_id_from=333.788.videopod.sections
  4. Androidstudio底部导航栏实现:https://www.bilibili.com/video/BV1XB4y1d7et/?spm_id_from=333.337.search-card.all.click&vd_source=984bb03f768809c7d33f20179343d8c8
  5. Android使用TabLayout+ViewPager2实现左右滑动切换:https://www.bilibili.com/video/BV1Mz4y1c7eX/?spm_id_from=333.337.search-card.all.click&vd_source=984bb03f768809c7d33f20179343d8c8
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浩宇软件开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值