看得到的真相往往可能是骗人的,但是代码不会. 如果程序中出现了”灵异”事件,一定是问题没有分析到位,亦或是分析问题的方向出错
最近在项目中,我需要在一个独立进程的不死服务中打开一个浮窗,但是浮窗不显示.但是在Activity中可以打开浮窗.这是问题的表现,其中不同点是创建WindowManager的Context不同,使用Activity的Context可以,使用Service和Application的Context都不可以.通过Log日志可以发现,addView()方法是执行了,但是View并没有显示出来.接下来,我就进行了各方位的尝试
一.分析中…
1.是否存在浮窗创建之后又被移除了?
屏蔽浮窗移除代码浮窗还是没有显示
2.参考网上提供方案把浮窗的创建过程放到服务中,即重启一个服务用于开启浮窗
代码如下,浮窗还是没有出现
class FloatWindowService : Service() {
companion object {
private const val TAG = "DaemonService"
const val NOTICE_ID = 100
fun openFloatWindowService(mContext: Context) {
val intent = Intent(mContext, FloatWindowService::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
mContext.startService(intent)
}
}
private val TAG = "FloatWindowManager"
/**
* 小悬浮窗View的实例
*/
@SuppressLint("StaticFieldLeak")
private var smallWindow: FloatWindowSmallView? = null
/**
* 小悬浮窗View的参数
*/
private var smallWindowParams: WindowManager.LayoutParams? = null
/**
* 用于控制在屏幕上添加或移除悬浮窗
*/
private var mWindowManager: WindowManager? = null
/**
* 是否有悬浮窗(包括小悬浮窗和大悬浮窗)显示在屏幕上。
*
* @return 有悬浮窗显示在桌面上返回true,没有的话返回false。
*/
var isWindowShowing: Boolean = false
override fun onCreate() {
super.onCreate()
smallWindow = FloatWindowSmallView(applicationContext)
smallWindowParams = WindowManager.LayoutParams()
mWindowManager = getWindowManager(applicationContext)
setSmallWindowParams(mWindowManager!!, smallWindowParams!!)
}
private fun setSmallWindowParams(windowManager: WindowManager,smallWindowParams: WindowManager.LayoutParams) {
val outMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(outMetrics)
val screenWidth = outMetrics.widthPixels
val screenHeight = outMetrics.heightPixels
smallWindowParams!!.type = WindowManager.LayoutParams.TYPE_PHONE
// smallWindowParams!!.format = PixelFormat.RGBA_8888
smallWindowParams.format = PixelFormat.TRANSPARENT
smallWindowParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
smallWindowParams!!.gravity = Gravity.LEFT or Gravity.TOP
smallWindowParams!!.width = 600
smallWindowParams!!.height = 600
smallWindowParams!!.x = 0
smallWindowParams!!.y = 0
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
smallWindow!!.setParams(smallWindowParams!!)
mWindowManager!!.addView(smallWindow, smallWindowParams)
return super.onStartCommand(intent, flags, startId)
}
/**
* 如果WindowManager还未创建,则创建一个新的WindowManager返回。否则返回当前已创建的WindowManager。
*
* @param context 必须为应用程序的Context.
* @return WindowManager的实例,用于控制在屏幕上添加或移除悬浮窗。
*/
private fun getWindowManager(context: Context): WindowManager {
if (mWindowManager == null) {
mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
}
return mWindowManager!!
}
override fun onBind(intent: Intent?): IBinder {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
3.WindowManager问题
在我们平时写代码的时候,经常要用到windowmanager这个类,一般是通过addview的方式,把一个view添加到一个窗口中,在系统中,比如toast,悬浮框,状态栏,通知栏,锁屏界面他们都不是平常的activity中的window,因此通过windowmanager 直接addview的方式来呈现给用户
WindowManager是一个接口,并继承了ViewManager接口.ViewManager有三个方法,
addView
,updateViewLayout
,removeView
,分别用于添加View,更新View的布局和移除View,由于WindowManager继承ViewManager,可见WindowManager也是对View进行管理,其中WindowManager有多了一个removeViewImmediate()
用于立刻移除View,用于Activity中,防止Activity销毁之后造成窗口溢出
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
而从打印的Log信息来看,addView()
是执行成功,没有异常的,所以浮窗是添加成功的.但是在创建浮窗和移除浮窗的时候,一定要先确认浮窗addView.如果浮窗已经添加了,再次添加会报异常;如果浮窗没有添加,在执行removeView(View view)
会报异常
4.浮窗不显示是因为浮窗的宽高为0
(1)将浮窗layout中的宽高赋值,而不是wrap_content
(2)将addView(View view, ViewGroup.LayoutParams params)
浮窗params的宽高写死为200,但是还是没有显示.但是点击浮窗所在的位置,会执行updateViewLayout
方法和浮窗的点击事件,由此更加确认浮窗已经存在
5.在浮窗的构造方法中添加setBackgroundColor(Color.GREEN)
这个时候出现了一个绿色的方块儿区域,但是图片还是没有显示出来
6.在layout中的布局中添加背景
背景显示出来了,图片未显示
7.将app:srcCompat
换为android:src
图片显示出来了,本来直接给出结果就可以了,但是我还是梳理了整个分析的流程.
接下来就是揭晓原因的时候了
使用 app:srcCompat 的时候 引入的图片显示不出来的解决方案
首先查看的你的Activity 继承的是那个Activity 如果是继承AppcompatActivity 使用 ImageView的 app:srcCompat 是没有问题的
如果你的Activity不是继承的AppcompatActivity, 需要用到 android.support.v7.widget.AppCompatImageView 代替 ImageView。
这样就可以解释为什么我可以通过Activity打开浮窗,而Service和Application的Context都不可以,当然我的Activity是继承AppcompatActivity.这里有两种修改方式,如下
方法一:修改ImageView
为AppCompatImageView
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/athanasy_float_window"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@mipmap/ic_launcher_round"
/>
</LinearLayout>
方法二:修改 app:srcCompat
为“““
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<ImageView
android:id="@+id/athanasy_float_window"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher_round"
/>
</LinearLayout>
二.浮窗源码:
1. FloatWindowSmallView
package com.hp.athanasyservice.xiaoqi.floatwindow
import android.content.Context
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.WindowManager
import android.widget.LinearLayout
import com.billy.cc.core.component.CC
import com.blankj.utilcode.util.ReflectUtils
import com.hp.athanasyservice.R
import com.hp.baseres.base.BaseApplication.Companion.mContext
import com.hp.baseres.utils.LogUtils
import com.hp.business.AppCCConstants
import kotlinx.android.synthetic.main.athanasy_small_float_window.view.*
/**
* Created by tangdekun on 2017/6/2.
*/
class FloatWindowSmallView(mContext: Context) : LinearLayout(mContext) {
/**
* 用于更新小悬浮窗的位置
*/
private val windowManager: WindowManager = mContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
/**
* 小悬浮窗的参数
*/
private var mParams: WindowManager.LayoutParams? = null
/**
* 记录当前手指位置在屏幕上的横坐标值
*/
private var xInScreen: Float = 0.toFloat()
/**
* 记录当前手指位置在屏幕上的纵坐标值
*/
private var yInScreen: Float = 0.toFloat()
/**
* 记录手指按下时在屏幕上的横坐标的值
*/
private var xDownInScreen: Float = 0.toFloat()
/**
* 记录手指按下时在屏幕上的纵坐标的值
*/
private var yDownInScreen: Float = 0.toFloat()
/**
* 记录手指按下时在小悬浮窗的View上的横坐标的值
*/
private var xInView: Float = 0.toFloat()
/**
* 记录手指按下时在小悬浮窗的View上的纵坐标的值
*/
private var yInView: Float = 0.toFloat()
private var screenWidth: Float = 0.toFloat()
private var screenHeight: Float = 0.toFloat()
init {
LayoutInflater.from(mContext).inflate(R.layout.athanasy_small_float_window, this)
viewWidth = athanasy_float_window.layoutParams.width
viewHeight = athanasy_float_window.layoutParams.height
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 手指按下时记录必要数据,纵坐标的值都需要减去状态栏高度
xInView = event.x
yInView = event.y
xDownInScreen = event.rawX
yDownInScreen = event.rawY - getStatusBarHeight()
xInScreen = event.rawX
yInScreen = event.rawY - getStatusBarHeight()
}
MotionEvent.ACTION_MOVE -> {
xInScreen = event.rawX
yInScreen = event.rawY - getStatusBarHeight()
// 手指移动的时候更新小悬浮窗的位置
updateViewPosition()
}
MotionEvent.ACTION_UP -> {
val t = System.currentTimeMillis().toFloat()
// 如果手指离开屏幕时,xDownInScreen和xInScreen相等,且yDownInScreen和yInScreen相等,则视为触发了单击事件。
updateViewPositionFinish()
LogUtils.d(TAG, "updateViewPositionFinish():" + (System.currentTimeMillis() - t))
if (xDownInScreen == xInScreen && yDownInScreen == yInScreen) {
openBigWindow()
}
LogUtils.d(TAG, "openBigWindow():" + (System.currentTimeMillis() - t))
}
}
return true
}
/**
* 更新浮窗移动完毕的位置
*/
private fun updateViewPositionFinish() {
mParams!!.y = (yInScreen - yInView).toInt()
screenWidth = windowManager.defaultDisplay.width.toFloat()
screenHeight = windowManager.defaultDisplay.height.toFloat()
if (xInScreen <= screenWidth / 2) {
mParams!!.x = 0
} else {
mParams!!.x = screenWidth.toInt()
}
windowManager.updateViewLayout(this, mParams)
mXProportion = mParams!!.x / screenWidth
mYProportion = mParams!!.y / screenHeight
}
/**
* 将小悬浮窗的参数传入,用于更新小悬浮窗的位置。
*
* @param params 小悬浮窗的参数
*/
fun setParams(params: WindowManager.LayoutParams) {
mParams = params
}
/**
* 更新小悬浮窗在屏幕中的位置。
*/
private fun updateViewPosition() {
mParams!!.x = (xInScreen - xInView).toInt()
mParams!!.y = (yInScreen - yInView).toInt()
windowManager.updateViewLayout(this, mParams)
}
/**
* 打开Activity
*/
private fun openBigWindow() {
CC.obtainBuilder(AppCCConstants.COMPONENT_NAME)
.setActionName(AppCCConstants.ACTION_OPEN_MAINACTIVITY)
.setContext(mContext)
.build()
.call()
}
/**
* 用于获取状态栏的高度。
*
* @return 返回状态栏高度的像素值。
*/
private fun getStatusBarHeight(): Int {
if (statusBarHeight == 0) {
try {
val x = ReflectUtils.reflect("com.android.internal.R\$dimen").field("status_bar_height").get<Int>()
statusBarHeight = resources.getDimensionPixelSize(x)
} catch (e: Exception) {
e.printStackTrace()
}
}
return statusBarHeight
}
companion object {
val TAG = FloatWindowSmallView::class.java.name!!
/**
* 记录每次浮窗当前的位置 --每次切换旋转屏幕按比例跳转位置
*/
var mXProportion = 1f
var mYProportion = 0.5f
/**
* 记录小悬浮窗的宽度
*/
var viewWidth: Int = 0
/**
* 记录小悬浮窗的高度
*/
var viewHeight: Int = 0
/**
* 记录系统状态栏的高度
*/
private var statusBarHeight: Int = 0
}
}
2. FloatWindowManager
package com.hp.athanasyservice.xiaoqi.floatwindow
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.PixelFormat
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.WindowManager
import android.view.WindowManager.LayoutParams
import com.hp.baseres.utils.LogUtils
/**
* Created by tangdekun on 2017/6/2.
*/
object FloatWindowManager {
private val TAG = "FloatWindowManager"
/**
* 小悬浮窗View的实例
*/
@SuppressLint("StaticFieldLeak")
private var smallWindow: FloatWindowSmallView? = null
/**
* 小悬浮窗View的参数
*/
private var smallWindowParams: LayoutParams? = null
/**
* 用于控制在屏幕上添加或移除悬浮窗
*/
private var mWindowManager: WindowManager? = null
/**
* 是否有悬浮窗(包括小悬浮窗和大悬浮窗)显示在屏幕上。
*
* @return 有悬浮窗显示在桌面上返回true,没有的话返回false。
*/
var isWindowShowing: Boolean = false
/**
* 创建一个小悬浮窗。初始位置为屏幕的右部中间位置。
*
* @param context 必须为应用程序的Context.
*/
fun createSmallWindow(context: Context) {
val windowManager = getWindowManager(context)
fun addView(context: Context, windowManager: WindowManager) {
LogUtils.dTag(TAG, "创建浮窗$context")
if (smallWindow == null) {
smallWindow = FloatWindowSmallView(context)
if (smallWindowParams == null) {
setSmallWindowParams(context)
}
smallWindow!!.setParams(smallWindowParams!!)
LogUtils.dTag(TAG, smallWindowParams.toString())
}
windowManager.addView(smallWindow, smallWindowParams)
isWindowShowing = true
}
if (!isWindowShowing) {
addView(context, windowManager)
} else {
LogUtils.dTag(TAG, "浮窗已存在$context")
}
}
/**
* 重置小浮窗位置
*
* @param context
*/
fun resetSmallWindowPosition(context: Context) {
LogUtils.dTag(TAG, "resetSmallWindowPosition")
if (smallWindowParams != null) {
val windowManager = getWindowManager(context)
val outMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(outMetrics)
val screenWidth = outMetrics.widthPixels
val screenHeight = outMetrics.heightPixels
smallWindowParams!!.x = (FloatWindowSmallView.mXProportion * screenWidth).toInt()
smallWindowParams!!.y = (FloatWindowSmallView.mYProportion * screenHeight).toInt()
if (smallWindow != null) {
getWindowManager(context).updateViewLayout(smallWindow, smallWindowParams)
}
}
}
private fun setSmallWindowParams(context: Context) {
val windowManager = getWindowManager(context)
val outMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(outMetrics)
val screenWidth = outMetrics.widthPixels
val screenHeight = outMetrics.heightPixels
smallWindowParams = LayoutParams()
smallWindowParams!!.type = WindowManager.LayoutParams.TYPE_PHONE
smallWindowParams!!.format = PixelFormat.RGBA_8888
smallWindowParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
smallWindowParams!!.gravity = Gravity.LEFT or Gravity.TOP
smallWindowParams!!.width = FloatWindowSmallView.viewWidth
smallWindowParams!!.height = FloatWindowSmallView.viewHeight
smallWindowParams!!.x = screenWidth
smallWindowParams!!.y = screenHeight / 2
}
/**
* 将小悬浮窗从屏幕上移除。
*
* @param context 必须为应用程序的Context.
*/
fun removeSmallWindow(context: Context) {
if (smallWindow != null && isWindowShowing) {
LogUtils.dTag(TAG, "删除浮窗$context")
val windowManager = getWindowManager(context)
windowManager.removeView(smallWindow)
isWindowShowing = false
smallWindow = null
smallWindowParams = null
} else {
LogUtils.dTag(TAG, "$context 浮窗不存在,无法删除")
}
}
/**
* 如果WindowManager还未创建,则创建一个新的WindowManager返回。否则返回当前已创建的WindowManager。
*
* @param context 必须为应用程序的Context.
* @return WindowManager的实例,用于控制在屏幕上添加或移除悬浮窗。
*/
private fun getWindowManager(context: Context): WindowManager {
if (mWindowManager == null) {
mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
}
return mWindowManager!!
}
}