原文章:Android 直播中的悬浮小窗以及封装
git地址:FloatWindow
原博主开发的是直播中的浮窗,本项目为im的视频聊天浮窗,略作修改。
FloatView
class FloatView(context: Context) : FrameLayout(context) {
private var lastX: Float = 0f
private var clickLastX: Float = 0f
private var lastY: Float = 0f
private var clickLastY: Float = 0f
private var minOffsetX = 0 // 最小偏移量
private var minOffsetY = 0
private var maxOffsetX = 0 // 最大偏移量
private var maxOffsetY = 0
private var offsetX: Int = minOffsetX // 当前偏移量
private var offsetY: Int = minOffsetY
private var windowManager: WindowManager? = null
private var layoutParams: WindowManager.LayoutParams = WindowManager.LayoutParams()
private val displayMetrics: DisplayMetrics = context.resources.displayMetrics
private val gestureListener = GestureListener()
private val gestureDetector = GestureDetectorCompat(context, gestureListener)
private val scroller = OverScroller(context)
private val flingRunnable = FlingRunnable()
private var mOnItemClickListener: OnItemClickListener? = null
init {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}
layoutParams.format = PixelFormat.TRANSPARENT
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
layoutParams.gravity = Gravity.START or Gravity.TOP
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT
}
fun setAdapter(adapter: FloatViewAdapter) {
removeAllViews()
adapter.onCreateView(LayoutInflater.from(context), this)
adapter.onBindView(this)
}
fun addWindow(windowManager: WindowManager) {
this.windowManager = windowManager
this.windowManager?.addView(this, layoutParams)
//因为浮窗默认在左上角,重新设置一个位置
onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, SizeUtils.dp2px(12f).toFloat(),
SizeUtils.dp2px(82f).toFloat(), 0))
}
fun remove() {
if (isAttachedToWindow) {
windowManager?.removeView(this)
windowManager = null
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
lastX = event.rawX
clickLastX = event.rawX
lastY = event.rawY
clickLastY = event.rawY
}
MotionEvent.ACTION_MOVE -> {
val currentX = event.rawX
val currentY = event.rawY
val dx = (currentX - lastX).toInt()
val dy = (currentY - lastY).toInt()
fixOffset(dx, dy)
move()
lastX = currentX
lastY = currentY
}
MotionEvent.ACTION_UP->{
if (clickLastX == lastX && clickLastY == lastY){
mOnItemClickListener?.onItemClick()
}
}
}
return gestureDetector.onTouchEvent(event)
}
private fun move() {
layoutParams.x = offsetX
layoutParams.y = offsetY
windowManager?.updateViewLayout(this, layoutParams)
}
private fun fixOffset(dx: Int, dy: Int) {
// 控制偏移量 0<=offset<=displayMetrics.widthPixels - width
val currentOffsetX = max(offsetX + dx, 0)
val currentOffsetY = max(offsetY + dy, 0)
maxOffsetX = if (maxOffsetX > 0) maxOffsetX else displayMetrics.widthPixels - width
maxOffsetY = if (maxOffsetY > 0) maxOffsetY else displayMetrics.heightPixels - height
offsetX = min(currentOffsetX, maxOffsetX)
offsetY = min(currentOffsetY, maxOffsetY)
}
inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?): Boolean {
return true
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
performClick()
return true
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
maxOffsetX = if (maxOffsetX > 0) maxOffsetX else displayMetrics.widthPixels - width
maxOffsetY = if (maxOffsetY > 0) maxOffsetY else displayMetrics.heightPixels - height
scroller.fling(
offsetX,
offsetY,
velocityX.toInt(),
velocityY.toInt(),
minOffsetX,
maxOffsetX,
minOffsetY,
maxOffsetY
)
postOnAnimation(flingRunnable)
return true
}
}
inner class FlingRunnable : Runnable {
override fun run() {
if (scroller.computeScrollOffset()) {
offsetX = scroller.currX
offsetY = scroller.currY
move()
postOnAnimation(this)
}
}
}
interface FloatViewAdapter {
fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View
fun onBindView(itemView: View)
}
fun setOnItemClickListener(onItemClickListener: OnItemClickListener) {
mOnItemClickListener = onItemClickListener
}
interface OnItemClickListener {
fun onItemClick()
}
}
FloatWindow
class FloatWindow(context: Context) {
companion object {
@Volatile
private var instance: FloatWindow? = null
fun getInstance(context: Context) = instance ?: synchronized(this) {
instance ?: FloatWindow(context.applicationContext).also { instance = it }
}
}
private var windowManager: WindowManager =
context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private var floatView: FloatView? = null
fun bindView(view: FloatView) {
this.floatView = view
floatView?.addWindow(windowManager)
}
fun removeView() {
floatView?.remove()
floatView = null
}
}
SimpleAdapter
class SimpleAdapter(var mLocalView: SurfaceView) : FloatView.FloatViewAdapter {
var mFrameLayout : FrameLayout ?= null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.layout_test, container)
}
override fun onBindView(itemView: View) {
itemView.findViewById<FrameLayout>(R.id.local_video_view_container).addView(mLocalView)
mFrameLayout = itemView.findViewById<FrameLayout>(R.id.local_video_view_container)
}
fun getFrameLayout(): FrameLayout? {
return mFrameLayout;
}
}
SimpleAdapter原来文章并无SurfaceView参数,本项目需要,所以略作修改,在FloatView中的OnItemClickListener监听也是新加的,需求是在视频页面点击切换视图,在其他页面点击回到视频页面。
记得在清单中给activity设置
android:launchMode="singleInstance"
android:taskAffinity=".taskCall"
android:excludeFromRecents ="true"