zl程序教程

您现在的位置是:首页 >  移动开发

当前栏目

Android kotlin实现悬浮窗拖动,LayoutParams参数不能为负数的解决方案

AndroidKotlin解决方案 实现 参数 不能 拖动 悬浮
2023-09-14 09:05:07 时间

目录

1.概述

2.kotlin实现悬浮窗拖动,LayoutParams参数不能为负数的解决方案的核心类

3.kotlin实现悬浮窗拖动,LayoutParams参数不能为负数的解决方案的核心功能实现和分析

  3.1 WindowManager.java中相关常量分析

3.2 kotlin实现悬浮窗功能

3.3WindowManager管理类 负责显示和关闭悬浮窗

3.4 Activity中启用悬浮窗


1.概述

  在用kotlin实现悬浮窗功能的时候,遇到过当设置WindowManager.LayoutParams的x坐标为负数时,它还是在左上角0的位置,x值为负数不起作用的情况,这就需要查看WindowManger的相关参数了

2.kotlin实现悬浮窗拖动,LayoutParams参数不能为负数的解决方案的核心类

frameworks/base/core/java/android/view/WindowManager.java

3.kotlin实现悬浮窗拖动,LayoutParams参数不能为负数的解决方案的核心功能实现和分析

  3.1 WindowManager.java中相关常量分析

          @Deprecated
          public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
  
          /** Window flag: as long as this window is visible to the user, keep
           *  the device's screen turned on and bright. */
          public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;
  
          /** Window flag: place the window within the entire screen, ignoring
           *  decorations around the border (such as the status bar).  The
           *  window must correctly position its contents to take the screen
           *  decoration into account.  This flag is normally set for you
           *  by Window as described in {@link Window#setFlags}.
           *
           *  <p>Note: on displays that have a {@link DisplayCutout}, the window may be placed
           *  such that it avoids the {@link DisplayCutout} area if necessary according to the
           *  {@link #layoutInDisplayCutoutMode}.
           */
          public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
  
          /** Window flag: allow window to extend outside of the screen. */
          public static final int FLAG_LAYOUT_NO_LIMITS   = 0x00000200;

在WindowManager的LayoutParams的参数中可以看出FLAG_LAYOUT_NO_LIMITS常量
就是可以允许超出屏幕之外的区域显示悬浮窗

3.2 kotlin实现悬浮窗功能

  自定义View
package com.pne.view

import android.content.Context
import android.os.Handler
import android.os.Message
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.view.ViewConfiguration
import android.view.WindowManager
import android.widget.LinearLayout
import com.pne.jnitest.R

/**
 *
 */
class DragScreenView(private val mContext: Context) : LinearLayout(mContext), View.OnClickListener {
    private var mWindowManager: WindowManager?=null
    private var mLayoutParams: WindowManager.LayoutParams? = null
    private var mLastDownTime: Long = 0
    private var mLastDownX = 0f
    private var mDownX = 0f
    private var mLastDownY = 0f
    private var mDownY = 0f
    private var mIsLongTouch = false
    private var mIsTouching = false
    private var mTouchSlop = 0f
    private var mStatusBarHeight = 0
    private var mCurrentMode = 0
    private val time = 0
    private var mOffsetToParent = 0
    private var mOffsetToParentY = 0
    private var mAddCount = 0
    private var mSubCount = 0
    private val mHandler: Handler = object : Handler() {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            when (msg.what) {
                0 -> if (mAddCount <= 20) {
                    mAddCount += 1
                    mLayoutParams!!.x = -200 + mAddCount * 10
                    mWindowManager?.updateViewLayout(this@DragScreenView, mLayoutParams) //不断刷新悬浮窗的位置
                    sendEmptyMessageDelayed(0, 50)
                }
                1 -> if (mSubCount <= 20) {
                    mSubCount += 1
                    mLayoutParams!!.x = -mSubCount * 10
                    mWindowManager?.updateViewLayout(this@DragScreenView, mLayoutParams) //不断刷新悬浮窗的位置
                    sendEmptyMessageDelayed(1, 50)
                }
            }
        }
    }

    private fun initView() {
        val view = View.inflate(context, R.layout.layout_ball, this)
        mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat()
        mCurrentMode = MODE_NONE
        mStatusBarHeight = statusBarHeight
        mOffsetToParent = dip2px(25f)
        mOffsetToParentY = mStatusBarHeight + mOffsetToParent
        mHandler.sendEmptyMessageDelayed(0, 200)
        view.setOnTouchListener(OnTouchListener { v, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    mIsTouching = true
                    mLastDownTime = System.currentTimeMillis()
                    mLastDownX = event.x
                    mLastDownY = event.y
                    postDelayed({
                        if (isLongTouch) {
                            mIsLongTouch = true
                        }
                    }, LONG_CLICK_LIMIT)
                }
                MotionEvent.ACTION_MOVE -> {
                    if (!mIsLongTouch && isTouchSlop(event)) {
                        return@OnTouchListener true
                    }
                    if (mIsLongTouch && (mCurrentMode == MODE_NONE || mCurrentMode == MODE_MOVE)) {
//                            mLayoutParams.x = (int) (event.getRawX() - mLastDownX);
//                            mLayoutParams.y = (int) (event.getRawY() - mLastDownY);
//                            mWindowManager.updateViewLayout(RecordScreenView.this, mLayoutParams);//不断刷新悬浮窗的位置
                        mCurrentMode = MODE_MOVE
                    }
                }
                MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
                    mIsTouching = false
                    if (mIsLongTouch) {
                        mIsLongTouch = false
                    }
                    mCurrentMode = MODE_NONE
                    mDownX = event.x
                    mDownY = event.y
                    if (mLastDownX - mDownX >= 30 && Math.abs(mDownY - mLastDownY) <= 30) {
                        mHandler.sendEmptyMessageDelayed(1, 100)
                    }
                }
            }
            true
        })
    }

    private val isLongTouch: Boolean
        private get() {
            val time = System.currentTimeMillis()
            return if (mIsTouching && mCurrentMode == MODE_NONE && time - mLastDownTime >= LONG_CLICK_LIMIT) {
                true
            } else false
        }

    /**
     * 判断是否是轻微滑动
     *
     * @param event
     * @return
     */
    private fun isTouchSlop(event: MotionEvent): Boolean {
        val x = event.x
        val y = event.y
        return if (Math.abs(x - mLastDownX) < mTouchSlop && Math.abs(y - mLastDownY) < mTouchSlop) {
            true
        } else false
    }

    fun setLayoutParams(params: WindowManager.LayoutParams?) {
        mLayoutParams = params
    }

    /**
     * 获取通知栏高度
     *
     * @return
     */
    private val statusBarHeight: Int
        private get() {
            var statusBarHeight = 0
            try {
                val c = Class.forName("com.android.internal.R\$dimen")
                val o = c.newInstance()
                val field = c.getField("status_bar_height")
                val x = field[o] as Int
                statusBarHeight = resources.getDimensionPixelSize(x)
            } catch (e: Exception) {
                e.printStackTrace()
            }
            return statusBarHeight
        }

    fun dip2px(dip: Float): Int {
        return TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP, dip, context.resources.displayMetrics
        ).toInt()
    }

    override fun onClick(view: View) {}

    companion object {
        private const val LONG_CLICK_LIMIT: Long = 20
        private const val TIME_COUNT = 0
        private const val MODE_NONE = 0x000
        private const val MODE_MOVE = 0x001
    }

    init {
        mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        initView()
    }
}

layout_ball.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="200px"
    android:layout_height="100px"
    android:orientation="vertical"
    android:background="@drawable/notification_bg">
    <TextView
        android:id="@+id/tv_itemdialog_ok"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical|center_horizontal"
        android:text="ok"
        android:textColor="@android:color/black"
        />
    <TextView
        android:id="@+id/tv_itemdialog_close"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical|center_horizontal"
        android:text="cancel"
        android:textColor="@android:color/black"
        />
</LinearLayout>

通过对view.setOnTouchListener的监听onTouch事件,负责对手势滑动的监听然后判断手势的x坐标滑动前后对比,然后做出左右滑动事件的响应,默认是从左边慢慢滑出悬浮窗的,左滑慢慢隐藏悬浮窗

3.3WindowManager管理类 负责显示和关闭悬浮窗

package com.pne.view

import android.content.Context
import android.graphics.PixelFormat
import android.view.Gravity
import android.view.WindowManager

object FloatWindowManager {
    private var mBallView: DragScreenView? = null
    private var mWindowManager: WindowManager? = null
    @JvmStatic
    fun addBallView(context: Context) {
        if (mBallView == null) {
            val windowManager = getWindowManager(context)
            val screenWidth = windowManager!!.defaultDisplay.width
            val screenHeight = windowManager.defaultDisplay.height
            mBallView = DragScreenView(context)
            val params = WindowManager.LayoutParams()
            params.x = -200
            params.y = 0
            params.width = WindowManager.LayoutParams.WRAP_CONTENT
            params.height = WindowManager.LayoutParams.WRAP_CONTENT
            params.gravity = Gravity.LEFT or Gravity.TOP
            params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
            params.format = PixelFormat.RGBA_8888
            params.flags = (WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
            mBallView!!.setLayoutParams(params)
            windowManager.addView(mBallView, params)
        }
    }

    fun removeBallView(context: Context) {
        if (mBallView != null) {
            val windowManager = getWindowManager(context)
            windowManager!!.removeView(mBallView)
            mBallView = null
        }
    }

    private fun getWindowManager(context: Context): WindowManager? {
        if (mWindowManager == null) {
            mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        }
        return mWindowManager
    }
}

FloatWindowManager 通过WindowManager调用addBallView(context: Context)添加悬浮窗,通过
removeBallView(context: Context)移除悬浮窗功能

3.4 Activity中启用悬浮窗

import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.widget.Toast;

import com.pne.view.FloatWindowManager;

public class FloatActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (Build.VERSION.SDK_INT >= 23) {
            //设置中请求开启悬浮窗权限
            if (!Settings.canDrawOverlays(this)) {
                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(intent);
            }else{
                initView();
            }
        }
    }

    private void initView() {
        FloatWindowManager.addBallView(FloatActivity.this);
        finish();
    }

在onCreate(Bundle savedInstanceState)中先是判断有没悬浮窗权限,然后启用悬浮窗功能