How easy is the Android floating window to implement?This is easy to do with kotlin!

Keywords: Android Windows github xml Attribute

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

Posted by mrdave on Wed, 15 Jan 2020 10:33:32 -0800