In this paper, we abstract a floating window tool class from scratch, which can be used to display floating Windows on any business interface. It can manage multiple float Windows simultaneously, and float Windows can respond to touch events, drag and drop, and have edge animations.
The example code in this article is written in Kotlin. The Kotlin tutorial series can be found here
The effect is as follows:
This is the first of a series of articles on Android Windows applications.
- For floating Window and the | Android suspension Windows application Window
- Subsided notification of an implementation | Android suspension Windows application Window
According to floating window
The native ViewManager interface provides methods for adding and manipulating views to Windows:
Public void addView(View View, viewgroup.layoutParams Params); Public void updateViewLayout(View View, viewGroup.layoutParams Params); Public void removeView(View View); }Copy the code
The template code for displaying the window using this interface is as follows:
Val windowView = LayoutInflater. From (context).inflate(R.Id.window_view, const) { Null) //' get WindowManager system service 'val WindowManager = Context.getSystemService (context.window_Service) as WindowManager / / 'building window layout parameters' WindowManager. LayoutParams (). The apply {type = WindowManager. LayoutParams. TYPE_APPLICATION width = WindowManager.LayoutParams.WRAP_CONTENT height = WindowManager.LayoutParams.WRAP_CONTENT gravity = Gravity.START or Let {layoutParams-> //' addView to window 'windowmanager.addview (windowView, layoutParams)}Copy the code
- The above code is displayed in the upper left corner of the current interface
R.id.window_view.xml
The layout defined in. - To avoid duplication, abstract this code into a function where the window view content and display location change as required, and parameterize it:
object FloatWindow{ private var context: Context? = null //' current window parameter 'var windowInfo: windowInfo? = null //' Class WindowInfo(var view: view? { var layoutParams: WindowManager.LayoutParams? Var height: Int = 0 var height: Int = 0 var hasView() = view! = null && layoutParams ! = null //' Does the view in the window have a father? 'fun hasParent() = hasView() && View? .parent ! = null} //' display 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 Windowinfo.layoutparams = createLayoutParam(x, y) //' display window 'if (! windowInfo.hasParent().value()) { val windowManager = this.context? .getSystemService(Context.WINDOW_SERVICE) as WindowManager windowManager.addView(windowInfo.view, Windowinfo.layoutparams)}} private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams { if (context == null) { return WindowManager.LayoutParams() } return WindowManager. LayoutParams (). The apply {/ / 'this type do not need to apply for permission 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 value for null Int 'fun Int? .value() = this ? : 0}Copy the code
- will
FloatWindow
The declaration becomes a singleton in order to easily display the float window on any interface throughout the life of the app. - In order to facilitate the unified management of window parameters, the inner class is abstracted
WindowInfo
- You can now display a float 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 the code
Float window background color
The product requires that the screen be dimmed when the float window is displayed. Set the WindowManager. LayoutParams. FLAG_DIM_BEHIND label with dimAmount can easily implement:
Object FloatWindow{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 / / 'sets floating window background dark WindowManager. LayoutParams. FLAG_DIM_BEHIND / /' to set the default dimming level 0, namely the same dark, 1 dimAmount = 0f gravity = gravity.START or gravity.TOP width = windowInfo? .width.value() height = windowInfo? This.value () this.x = x this.y = y}} //' for the business interface to adjust the Float window background as needed 'fun setDimAmount(amount:Float){windowInfo? .layoutParams? .let { it.dimAmount = amount } } }Copy the code
Set the float window click event
Setting the click event for the float window is equivalent to setting the click event for the float window view, but if you use setOnClickListener() directly on the float window view, the float window touch event will not be responded to, and drag and drop will not work. So you have to start with the lower-level touch events:
Object FloatWindow: view.onTouchListener {//' display 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 touch listener for float window view '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 the code
- in
onTouch(v: View, event: MotionEvent)
For more detailed touch events, for exampleACTION_DOWN
.ACTION_MOVE
,ACTION_UP
. This facilitates the drag-and-drop implementation, but the capture of click events is complicated by the need to define the sequence in which the above three actions appear to be considered click events. Fortunately,GestureDetector
Did this for us:
Public class GestureDetector {public interface OnGestureListener {//'ACTION_DOWN event 'Boolean onDown(MotionEvent e); Boolean onSingleTapUp(MotionEvent e); Boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY); . }}Copy the code
Building an instance of GestureDetector and passing the MotionEvent to it resolves the touch event into the upper-level event 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 WindowClickListener) { clickListener = listener } override fun onTouch(v: View, event: MotionEvent): Boolean {/ / 'delivers the touch events to GestureDetector analytic GestureDetector. OnTouchEvent (event) return true} / /' memories start touching point coordinates' private fun onActionDown(event: MotionEvent) { lastTouchX = event.rawX.toInt() lastTouchY = event.rawY.toInt() } private class GestureListener : GestureDetector. OnGestureListener {/ / 'memories start touch point coordinate override fun onDown (e: MotionEvent) : Boolean { onActionDown(e) return false } override fun onSingleTapUp(e: MotionEvent): Boolean {//' call listener when click event occurs' return clickListener? .onWindowClick(windowInfo) ? : false } ... } // interface WindowClickListener {fun onWindowClick(windowInfo: windowInfo?) : Boolean } }Copy the code
Drag the floating window
The ViewManager provides an updateViewLayout(View View, viewGroup.layoutParams Params) for updating the float window position, so you can do this by simply listening for ACTION_MOVE events and updating the float window View position in real time. GestureDetector ACTION_MOVE events are parsed into 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 {/ / 'delivers the touch events to GestureDetector analytic 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 {//' onActionMove(e2) return true}} private fun onActionMove(event: Float): Boolean {//' onActionMove(e2) return true}} private fun onActionMove(event: MotionEvent) {//' Get the current finger coordinate 'val currentX = event.rawx.toint () val currentY = event.rawy.toint () //' Get the finger move increment' val dx = Currentx-lasttouchx val dy = currenty-lasttouchy //' apply the move increment to the window layout parameter '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 float window movement 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 location windowManager. UpdateViewLayout (windowInfo? .view, windowInfo? .layoutParams) lastTouchX = currentX lastTouchY = currentY } }Copy the code
Automatic edging of float window
The new requirement is that when the float window is released, it needs to be automatically attached to the edge.
Think of the welt as a horizontal displacement animation. Figure out the starting point and end point abscesses of the animation when the hand is released, and use the animation values to constantly update the floating window position:
object FloatWindow : View.OnTouchListener{ private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener()) private var lastTouchX: Int = 0 private var lastTouchY: Int = 0 private var weltAnimator: ValueAnimator? = null override fun onTouch(v: View, event: MotionEvent): Boolean {/ / 'delivers the touch events to GestureDetector analytic GestureDetector. OnTouchEvent (event) / /' processing ACTION_UP event 'val action = event. The 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} val upX = event.rawx.toint () // val endX = if (upX > screenWidth / 2) {screenwidth-width} else {0} 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 values for empty Boolean fun Boolean? .value() = this ? : false }Copy the code
GestureDetector
After the parsingACTION_UP
The event was swallowed up, so I had toonTouch()
Intercept it in.- According to the size relationship between the horizontal axis of the hand and the horizontal axis of the midpoint of the screen, to decide whether the float window is attached to the left or right.
Manage multiple float Windows
If different service interfaces of the APP need to display the float window at the same time, float window A is displayed when you enter interface A, and then it is dragged to the lower right corner. When you exit interface A and enter interface B, float window B is displayed. When you enter interface A again, you are expected to restore the position of float window A when you left last time.
In the current FloatWindow, the windowInfo member is used to store a single FloatWindow parameter. To manage multiple float Windows at the same time, all FloatWindow parameters need to be stored in the Map structure, distinguished by tags:
Object FloatWindow: view.onTouchListener {private var windowInfoMap: HashMap<String, WindowInfo? > = HashMap() //' current float window parameter 'var windowInfo: windowInfo? Fun show(context: context, //' float window 'tag: String, //' float window' tag: String, //' WindowInfo? = windowInfoMap[tag], x: Int = windowInfo? .layoutParams? .x.value(), y: Int = windowInfo? .layoutParams? .y.windowindoue ()) {if (windowInfo == null) {return} if (Windowinfo.view == null) {return} //' update current float window parameter 'this.windowinfo = 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 the code
When the float window is displayed, add the tag parameter to uniquely identify the float window, and provide the default parameter for windowInfo. When the original float window is restored, the windowInfo parameter may not be provided. FloatWindow will go to the “windowInfoMap” and find the corresponding “windowInfo” based on the given tag.
Listen for float window out-of-bounds click events
The new demand comes, when you click on the float window, the side of the float window is displayed like a drawer, when you click on the area outside the float window, the drawer is put away.
When I first received this new request, I had no idea. Now, PopupWindow 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 the code
This function sets whether touch events outside the window boundary are allowed to be passed to the window. Tracking the mOutsideTouchable variable should give you 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); . //' If out of bounds can be touched, Will FLAG_WATCH_OUTSIDE_TOUCH assigned to flag 'if (mOutsideTouchable) {curFlags | = WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; }... }}Copy the code
Continue tracing up to where computeFlags() was called:
public class PopupWindow { protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) { final WindowManager.LayoutParams p = new WindowManager.LayoutParams(); p.gravity = computeGravity(); 'p.lags = computeFlags(p.lags); p.type = mWindowLayoutType; p.token = token; . }}Copy the 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; / / 'building window layout parameters' final WindowManager. LayoutParams p = createPopupLayoutParams (token); preparePopup(p); p.x = x; p.y = y; invokePopup(p); }}Copy the code
You want to keep searching in the source code, but at FLAG_WATCH_OUTSIDE_TOUCH, the clue is broken. All we know now is that in order for the out-of-bounds click event to be passed to the window, we must set FLAG_WATCH_OUTSIDE_TOUCH for the layout parameter. But where should the event response logic go?
When the PopupWindow. SetOutsideTouchable (true), out after click in the window, the window will disappear. This must be calling dismiss(), and going up the call chain in dismiss() is bound to find the response logic for the out-of-bounds click:
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; //' Dismiss the window if an out-of-line touch event occurs'} else if (event.getAction() == motionEvent.action_outside) {dismiss(); return true; } else { return super.onTouchEvent(event); }}}}Copy the code
So just capture ACTION_OUTSIDE in the touch event callback in the root view of the window:
Object FloatWindow: view.onTouchListener {private var onTouchOutside: (() -> Unit)? '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 {//' out-of-bounds touch event handling 'if (event.action == motionEvent.action_outside) {onTouchOutside? Invoke () return true} / / 'click and drag and drop 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 the code
talk is cheap, show me the code
The example code hides unimportant details, and the full code is available at the link above.