Starting with business applications, this paper abstracts a floating window tool class from scratch that can be used to display floating windows on any business interface.It can manage multiple floating windows at the same time, and floating windows can respond to touch events, can be dragged, and have side animations.
The sample code is written in kotlin and the kotlin series of tutorials can be clicked Here
The results are as follows:
Show Floating Window
The native ViewManager interface provides a way to add and manipulate Views to windows:
public interface ViewManager{ //'Add View to Window' public void addView(View view, ViewGroup.LayoutParams params); //'Update View in Window' public void updateViewLayout(View view, ViewGroup.LayoutParams params); //'Remove View from Window' public void removeView(View view); } //Copy Code
Use this interface to display the template code for the window as follows:
//'Parse layout file as view' val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null) //'Get WindowManager System Services' val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager //'Build Window Layout Parameters' WindowManager.LayoutParams().apply { type = WindowManager.LayoutParams.TYPE_APPLICATION width = WindowManager.LayoutParams.WRAP_CONTENT height = WindowManager.LayoutParams.WRAP_CONTENT gravity = Gravity.START or Gravity.TOP x = 0 y = 0 }.let { layoutParams-> //'Add View to Window' windowManager.addView(windowView, layoutParams) } //Copy Code
- The above code shows the layout defined in R.id.window_view.xml in the upper left corner of the current interface.
- To avoid duplication, abstract this code into a function where the content and display location of the window view changes as required and parameterize it:
object FloatWindow{ private var context: Context? = null //'Current Window Parameters' var windowInfo: WindowInfo? = null //'Package parameters related to the Window s layout into an internal class' class WindowInfo(var view: View?) { var layoutParams: WindowManager.LayoutParams? = null //'Window Width' var width: Int = 0 //'Window height' var height: Int = 0 //'Is there a view in the window' fun hasView() = view != null && layoutParams != null //'window view whether there is a Father' fun hasParent() = hasView() && view?.parent != null } //'Show window' fun show( context: Context, windowInfo: WindowInfo?, x: Int = windowInfo?.layoutParams?.x.value(), y: Int = windowInfo?.layoutParams?.y.value(), ) { if (windowInfo == null) { return } if (windowInfo.view == null) { return } this.windowInfo = windowInfo this.context = context //'Create window layout parameters' windowInfo.layoutParams = createLayoutParam(x, y) //'Show window' if (!windowInfo.hasParent().value()) { val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager windowManager.addView(windowInfo.view, windowInfo.layoutParams) } } //'Create window layout parameters' private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams { if (context == null) { return WindowManager.LayoutParams() } return WindowManager.LayoutParams().apply { //'This type does not require permission to apply for' type = WindowManager.LayoutParams.TYPE_APPLICATION format = PixelFormat.TRANSLUCENT flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS gravity = Gravity.START or Gravity.TOP width = windowInfo?.width.value() height = windowInfo?.height.value() this.x = x this.y = y } } //'Provide default values for empty Int s' fun Int?.value() = this ?: 0 } //Copy Code
- FloatWindow is declared as a singleton for the purpose of making it easy for any interface to display floating windows throughout the app life cycle.
- To facilitate the unified management of window parameters, the internal class WindowInfo is abstracted
- Now you can display a floating window in the upper left corner of the screen like this:
val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null) WindowInfo(windowView).apply{ width = 100 height = 100 }.let{ windowInfo -> FloatWindow.show(context, windowInfo, 0, 0) } //Copy Code
Floating window background color
The product requires the screen to dim when the floating window is displayed.Setting the WindowManager.LayoutParams.FLAG_DIM_BEHIND tag with dimAmount makes it easy to:
object FloatWindow{ //Current window parameters var windowInfo: WindowInfo? = null private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams { if (context == null) { return WindowManager.LayoutParams() } return WindowManager.LayoutParams().apply { type = WindowManager.LayoutParams.TYPE_APPLICATION format = PixelFormat.TRANSLUCENT flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or //'Set floating window background dimming' WindowManager.LayoutParams.FLAG_DIM_BEHIND //'Set the default dimming level to 0, i.e. no dimming, 1 for all black' dimAmount = 0f gravity = Gravity.START or Gravity.TOP width = windowInfo?.width.value() height = windowInfo?.height.value() this.x = x this.y = y } } //'For business interface to adjust floating window background brightness and darkness when needed' fun setDimAmount(amount:Float){ windowInfo?.layoutParams?.let { it.dimAmount = amount } } } //Copy Code
Set Floating Window Click Event
Setting a click event for a floating window is equivalent to setting a click event for a floating window view, but if setOnClickListener() is used directly for a floating window view, the floating window's touch event will not be responded to and dragging will not be possible.So you can only start with touch events at a lower level:
object FloatWindow : View.OnTouchListener{ //'Show window' fun show( context: Context, windowInfo: WindowInfo?, x: Int = windowInfo?.layoutParams?.x.value(), y: Int = windowInfo?.layoutParams?.y.value(), ) { if (windowInfo == null) { return } if (windowInfo.view == null) { return } this.windowInfo = windowInfo this.context = context //'Set up touch monitors for floating window views' windowInfo.view?.setOnTouchListener(this) windowInfo.layoutParams = createLayoutParam(x, y) if (!windowInfo.hasParent().value()) { val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager windowManager.addView(windowInfo.view, windowInfo.layoutParams) } } override fun onTouch(v: View, event: MotionEvent): Boolean { return false } } //Copy Code
- More detailed touch events such as ACTION_DOWN, ACTION_MOVE, ACTION_UP are available in onTouch(v: View, event: MotionEvent).This facilitates drag-and-drop implementation, but the capture of click events becomes more complex because you need to define how the three ACTIONs above are determined to be click events when they occur in what sequence.Fortunately, GestureDetector did this for us:
public class GestureDetector { public interface OnGestureListener { //'ACTION_DOWN Event' boolean onDown(MotionEvent e); //'Click Event' boolean onSingleTapUp(MotionEvent e); //'Drag Event' boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY); ... } } //Copy Code
Building a GestureDetector instance and passing MotionEvent to it resolves touch events into upper-level events of interest:
object FloatWindow : View.OnTouchListener{ private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener()) private var clickListener: WindowClickListener? = null private var lastTouchX: Int = 0 private var lastTouchY: Int = 0 //'Set click listeners for floating windows' fun setClickListener(listener: WindowClickListener) { clickListener = listener } override fun onTouch(v: View, event: MotionEvent): Boolean { //'Pass touch events to GestureDetector resolution' gestureDetector.onTouchEvent(event) return true } //'Memory starting touch point coordinates' private fun onActionDown(event: MotionEvent) { lastTouchX = event.rawX.toInt() lastTouchY = event.rawY.toInt() } private class GestureListener : GestureDetector.OnGestureListener { //'Memory starting touch point coordinates' override fun onDown(e: MotionEvent): Boolean { onActionDown(e) return false } override fun onSingleTapUp(e: MotionEvent): Boolean { //'Call the listener when the click event occurs' return clickListener?.onWindowClick(windowInfo) ?: false } ... } //'Floating window click listener' interface WindowClickListener { fun onWindowClick(windowInfo: WindowInfo?): Boolean } } //Copy Code
Drag floating window
ViewManager provides updateViewLayout(View view, ViewGroup.LayoutParams params) to update the floating window position, so drag and drop can be achieved by simply listening for ACTION_MOVE events and updating the floating window view position in real time.The ACTION_MOVE event is resolved by GestureDetector to OnGestureListener.onScroll() callback:
object FloatWindow : View.OnTouchListener{ private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener()) private var lastTouchX: Int = 0 private var lastTouchY: Int = 0 override fun onTouch(v: View, event: MotionEvent): Boolean { //'Pass touch events to GestureDetector resolution' gestureDetector.onTouchEvent(event) return true } private class GestureListener : GestureDetector.OnGestureListener { override fun onDown(e: MotionEvent): Boolean { onActionDown(e) return false } override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY:Float): Boolean { //'Respond to finger scrolling events' onActionMove(e2) return true } } private fun onActionMove(event: MotionEvent) { //'Get current finger coordinates' val currentX = event.rawX.toInt() val currentY = event.rawY.toInt() //'Get finger movement increments' val dx = currentX - lastTouchX val dy = currentY - lastTouchY //'Apply move increments to window layout parameters' windowInfo?.layoutParams!!.x += dx windowInfo?.layoutParams!!.y += dy val windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager var rightMost = screenWidth - windowInfo?.layoutParams!!.width var leftMost = 0 val topMost = 0 val bottomMost = screenHeight - windowInfo?.layoutParams!!.height - getNavigationBarHeight(context) //'Restrict the floating window moving area to the screen' if (windowInfo?.layoutParams!!.x < leftMost) { windowInfo?.layoutParams!!.x = leftMost } if (windowInfo?.layoutParams!!.x > rightMost) { windowInfo?.layoutParams!!.x = rightMost } if (windowInfo?.layoutParams!!.y < topMost) { windowInfo?.layoutParams!!.y = topMost } if (windowInfo?.layoutParams!!.y > bottomMost) { windowInfo?.layoutParams!!.y = bottomMost } //'Update floating window position' windowManager.updateViewLayout(windowInfo?.view, windowInfo?.layoutParams) lastTouchX = currentX lastTouchY = currentY } } //Copy Code
Floating window auto-edge
New demands are coming. After you drag the floating window loose, it needs to be automatically edged.
Understand edge-clipping as a horizontal displacement animation.When releasing the hand, the horizontal coordinates of the starting and ending points of the animation are calculated, and the position of the floating window is updated continuously with the animation values:
object FloatWindow : View.OnTouchListener{ private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener()) private var lastTouchX: Int = 0 private var lastTouchY: Int = 0 //'Edge-Sticking Animation' private var weltAnimator: ValueAnimator? = null override fun onTouch(v: View, event: MotionEvent): Boolean { //'Pass touch events to GestureDetector resolution' gestureDetector.onTouchEvent(event) //'Handle ACTION_UP events' val action = event.action when (action) { MotionEvent.ACTION_UP -> onActionUp(event, screenWidth, windowInfo?.width ?: 0) else -> { } } return true } private fun onActionUp(event: MotionEvent, screenWidth: Int, width: Int) { if (!windowInfo?.hasView().value()) { return } //'Record raise hand transverse coordinates' val upX = event.rawX.toInt() //'Edge Sticking Animation End Point Coordinate' val endX = if (upX > screenWidth / 2) { screenWidth - width } else { 0 } //'Build Edge-Sticking Animation' if (weltAnimator == null) { weltAnimator = ValueAnimator.ofInt(windowInfo?.layoutParams!!.x, endX).apply { interpolator = LinearInterpolator() duration = 300 addUpdateListener { animation -> val x = animation.animatedValue as Int if (windowInfo?.layoutParams != null) { windowInfo?.layoutParams!!.x = x } val windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager //'Update window location' if (windowInfo?.hasParent().value()) { windowManager.updateViewLayout(windowInfo?.view, windowInfo?.layoutParams) } } } } weltAnimator?.setIntValues(windowInfo?.layoutParams!!.x, endX) weltAnimator?.start() } //Provide default value for empty Boolean fun Boolean?.value() = this ?: false } //Copy Code
- The ACTION_UP event is swallowed after GestureDetector parses, so it can only be intercepted in onTouch().
- Depending on the size relationship between the raise-hand horizontal coordinate and the horizontal coordinate of the midpoint on the screen, it is decided whether the floating window is to the left or right.
Manage multiple floating windows
If different business interfaces of app need to show floating window at the same time: Floating window A is displayed when entering interface A, then it is dragged to the lower right corner, exit interface A, enter interface B, display floating window B, when entering interface A again, expect to restore the position of floating window A when last leaving.
Currently in FloatWindow, a single floating window parameter is stored in a windowInfo member. In order to manage multiple floating windows at the same time, all floating window parameters need to be saved in the Map structure and distinguished by tag:
object FloatWindow : View.OnTouchListener { //'Floating window parameter container' private var windowInfoMap: HashMap<String, WindowInfo?> = HashMap() //'Current Floating Window Parameters' var windowInfo: WindowInfo? = null //'Show Floating Window' fun show( context: Context, //'Floating window label' tag: String, //'Get the last saved parameter of the tag from the parameter container if no floating window parameter is provided' windowInfo: WindowInfo? = windowInfoMap[tag], x: Int = windowInfo?.layoutParams?.x.value(), y: Int = windowInfo?.layoutParams?.y.value() ) { if (windowInfo == null) { return } if (windowInfo.view == null) { return } //'Update current floating window parameters' this.windowInfo = windowInfo //'Save floating window parameters in containers' windowInfoMap[tag] = windowInfo windowInfo.view?.setOnTouchListener(this) this.context = context windowInfo.layoutParams = createLayoutParam(x, y) if (!windowInfo.hasParent().value()) { val windowManager =this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager windowManager.addView(windowInfo.view, windowInfo.layoutParams) } } } //Copy Code
When displaying floating windows, add tag tag tag parameter to uniquely identify the floating window and provide default parameter for windowInfo. When restoring the original floating window, you can leave the windowInfo parameter unavailable, and FloatWindow will go to windowInfoMap to find the corresponding windowInfo based on the given tag.
Listen for clicks outside floating windows
New demands are coming. When you click on a floating window, the adjacent floating window displays like a drawer. When you click on an area outside the floating window, the drawer receives.
When I first got this new demand, I had no idea.Recall that Popup Window has a setOutsideTouchable():
public class PopupWindow { /** * <p>Controls whether the pop-up will be informed of touch events outside * of its window. * * @param touchable true if the popup should receive outside * touch events, false otherwise */ public void setOutsideTouchable(boolean touchable) { mOutsideTouchable = touchable; } } //Copy Code
This function is used to set whether touch events outside the window boundary are allowed to be passed to the window.Tracking the mOutsideTouchable variable should lead to more clues:
public class PopupWindow { private int computeFlags(int curFlags) { curFlags &= ~( WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH); ... //'assign FLAG_WATCH_OUTSIDE_TOUCH to flag if it is touchable outside the Boundaries' if (mOutsideTouchable) { curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; } ... } } //Copy Code
Continue to track where computeFlags() calls are made:
public class PopupWindow { protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) { final WindowManager.LayoutParams p = new WindowManager.LayoutParams(); p.gravity = computeGravity(); //'Calculate and assign the window layout parameter flag attribute' p.flags = computeFlags(p.flags); p.type = mWindowLayoutType; p.token = token; ... } } //Copy Code
createPopupLayoutParams() is called when the window is displayed:
public class PopupWindow { public void showAtLocation(IBinder token, int gravity, int x, int y) { if (isShowing() || mContentView == null) { return; } TransitionManager.endTransitions(mDecorView); detachFromAnchor(); mIsShowing = true; mIsDropdown = false; mGravity = gravity; //'Build Window Layout Parameters' final WindowManager.LayoutParams p = createPopupLayoutParams(token); preparePopup(p); p.x = x; p.y = y; invokePopup(p); } } //Copy Code
You want to continue searching in the source code, but by FLAG_WATCH_OUTSIDE_TOUCH, the clue is broken.Now you only know that FLAG_WATCH_OUTSIDE_TOUCH must be set for the layout parameters in order for the out-of-bounds click event to be passed to the window.But where should the event response logic be written?
When PopupWindow.setOutsideTouchable(true) is called, the window disappears when clicked outside the window boundary.This is necessarily a call to dismiss(), which follows the invocation chain of dismiss() to find the response logic for the out-of-bounds click event:
public class PopupWindow { //'Window Root View' private class PopupDecorView extends FrameLayout { //'Window Root View Touch Events' @Override public boolean onTouchEvent(MotionEvent event) { final int x = (int) event.getX(); final int y = (int) event.getY(); if ((event.getAction() == MotionEvent.ACTION_DOWN) && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) { dismiss(); return true; //'Disband the window if an out-of-bounds touch event occurs' } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { dismiss(); return true; } else { return super.onTouchEvent(event); } } } } //Copy Code
So just capture ACTION_OUTSIDE in the touch event callback of the window root view:
object FloatWindow : View.OnTouchListener { //'Outside Touch Event Callback' private var onTouchOutside: (() -> Unit)? = null //'Set whether to respond to out-of-bounds click events' fun setOutsideTouchable(enable: Boolean, onTouchOutside: (() -> Unit)? = null) { windowInfo?.layoutParams?.let { layoutParams -> layoutParams.flags = layoutParams.flags or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH this.onTouchOutside = onTouchOutside } } override fun onTouch(v: View, event: MotionEvent): Boolean { //'Outside Touch Event Handling' if (event.action == MotionEvent.ACTION_OUTSIDE) { onTouchOutside?.invoke() return true } //'Click and drag event handling' gestureDetector.onTouchEvent(event).takeIf { !it }?.also { //there is no ACTION_UP event in GestureDetector val action = event.action when (action) { MotionEvent.ACTION_UP -> onActionUp(event, screenWidth, windowInfo?.width ?: 0) else -> { } } } return true } } //Copy Code
Last
Push on my GitHub dating address: https://github.com/Meng997998/AndroidJX
Start once