When a new message comes to you as you browse the public account, the notification will appear as a top-down animation at the top of the screen, and it is a global floating window across the interface (the effect is shown below). Although the abstract floating window utility class in the previous article already fulfills this requirement. But this article goes on to encapsulate some more user-friendly apis to implement sunken notifications.

This is the second article in a series of Android Window apps.

  1. For floating Window and the | Android suspension Windows application Window
  2. Subsided notification of an implementation | Android suspension Windows application Window

Predefined Common Locations

In the previous article, the abstract show() interface can precisely display floating Windows anywhere on the screen by specifying x and y. But sometimes the business requirements are vague, such as “float window in the right center of the screen.” If we can add an API and pre-define some common locations, the business layer can not worry about the calculation of window coordinates.

There are four common directions on the screen: up, down, left, and right, each of which can have three types of gravity: start point, midpoint, and end point. Put it together and you have 12 common positions.

Of course you can define 12 constants, with values ranging from 0 to 11. However, when a gravity is added at each location, four constants are added. This problem can be solved by dividing up, down, left, right and gravity into two groups:

object FloatWindow : OnTouchListener {// const val POSIITION_TOP = 1 const val POSITION_LEFT = 2 const val POSITION_RIGHT = 3 Const val POSITION_BOTTOM = 4 //'gravity 'const val GRAVITY_START = 100 const val GRAVITY_MID = 101 const val GRAVITY_END = 102 }Copy the code

Override a show() function with the usual positional arguments:

object FloatWindow : View.OnTouchListener { fun show( context: Context, tag: String, windowInfo: WindowInfo? = windowInfoMap[tag], //' new parameter 'position: Int, //' new parameter: gravity' gravity: Int){//' when (position){POSITION_TOP -> {when (gravity) -> {GRAVITY_START -> {... } GRAVITY_MID -> {... } GRAVITY_END -> {... } else -> {... } } } POSITION_LEFT -> { when (gravity) -> { GRAVITY_START -> {... } GRAVITY_MID -> {... } GRAVITY_END -> {... } else -> {... } } } POSITION_RIGHT -> { when (gravity) -> { GRAVITY_START -> {... } GRAVITY_MID -> {... } GRAVITY_END -> {... } else -> {... } } } POSITION_BOTTOM -> { when (gravity) -> { GRAVITY_START -> {... } GRAVITY_MID -> {... } GRAVITY_END -> {... } else -> {... } } } else -> {... } //' show(context, tag, windowInfo, x, y, false)}}Copy the code

That’s fine, but the show() function adds two new arguments that together represent a single, complete semantic representation: the location of the window.

Binary bits manage multiple states

Is there any way to combine two parameters into one? There are! A better solution lies in the View source code:

Public class View {/ 'state constants' * * * | -- -- -- -- -- -- -- | -- -- -- -- -- -- - | -- -- -- -- -- - | -- -- -- -- -- - | PFLAG_FOCUSED PFLAG_WANTS_FOCUS * 1 * 1 * 1 PFLAG_SELECTED * 1 PFLAG_IS_ROOT_NAMESPACE * 1 PFLAG_HAS_BOUNDS * 1 PFLAG_DRAWN * 1 PFLAG_DRAW_ANIMATION * 1 PFLAG_SKIP_DRAW * 1 PFLAG_REQUEST_TRANSPARENT_REGIONS * 1 PFLAG_DRAWABLE_STATE_DIRTY * 1 PFLAG_MEASURED_DIMENSION_SET * 1 PFLAG_FORCE_LAYOUT * 1 PFLAG_LAYOUT_REQUIRED * 1 PFLAG_PRESSED * 1 PFLAG_DRAWING_CACHE_VALID * 1 PFLAG_ANIMATION_STARTED * 1 PFLAG_SAVE_STATE_CALLED * 1 PFLAG_ALPHA_SET * 1 PFLAG_SCROLL_CONTAINER * 1 PFLAG_SCROLL_CONTAINER_ADDED * 1 PFLAG_DIRTY * 1 PFLAG_DIRTY_MASK * 1 PFLAG_OPAQUE_BACKGROUND * 1 PFLAG_OPAQUE_SCROLLBARS * 11 PFLAG_OPAQUE_MASK * 1 PFLAG_PREPRESSED * 1 PFLAG_CANCEL_NEXT_UP_EVENT * 1 PFLAG_AWAKEN_SCROLL_BARS_ON_ATTACH * 1 PFLAG_HOVERED * 1 PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK * 1 PFLAG_ACTIVATED * 1 PFLAG_INVALIDATED * |-------|-------|-------|-------| */ /** {@hide} */ static final int PFLAG_WANTS_FOCUS = 0x00000001;  /** {@hide} */ static final int PFLAG_FOCUSED = 0x00000002; /** {@hide} */ static final int PFLAG_SELECTED = 0x00000004; /** {@hide} */ static final int PFLAG_IS_ROOT_NAMESPACE = 0x00000008; //' current state 'public int mPrivateFlags; }Copy the code

The View stores all of its state bits in a variable of type int called mPrivateFlags. Int takes up four bytes, and one byte contains eight bits of binary, so it can store 32 binary states.

A state constant is also an int value, and each state constant is associated with only one of the 32 bits, which View represents as an 8-bit hexadecimal. (one hexadecimal bit is equivalent to four binary bits, for example 🙂

hexadecimal binary
1 0001
2 0010
3 0011

I used to define a state bit as an int, but now I can condense 32 int state values into a single int.

To add a state, simply perform a bit or operation:

mPrivateFlags |= PFLAG_DRAWN;
Copy the code

To determine whether the current state has a certain state, only bits and operations are needed:

public boolean hasFocus() { return (mPrivateFlags & PFLAG_FOCUSED) ! = 0; }Copy the code

To delete the state, simply take the reverse addbit and operation:

mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
Copy the code

Using binary bits to manage a large number of states not only saves memory, but also makes it easy to change and judge states, and to express complex states

Although the current business scenario contains only one state, the common position of the float window, it is a compound state containing position and gravity, which can be simplified using binary bit management:

object FloatWindow : View.OnTouchListener {//' gravity with 0-3 bits' const val FLAG_START = 0x00000001 const val FLAG_MID = 0x00000002 const val Const val FLAG_TOP = 0x00000010 const val FLAG_LEFT = 0x00000020 const val FLAG_RIGHT = 0x00000040 const val FLAG_BOTTOM = 0x00000080 }Copy the code

This simplifies the argument table for show() :

Object FloatWindow: view. OnTouchListener {fun show(context: context, tag: String, // windowInfo: WindowInfo? = windowInfoMap[tag], //'flag contains location and gravity information 'flag: Int) {//' windowInfo (flag, windowInfo, offset). Let {show(context, tag, windowInfo, it. false) } } }Copy the code

The parse of the flag is written in getShowPoint() :

object FloatWindow : View.OnTouchListener { private fun getShowPoint(flag: Int, windowInfo: WindowInfo?) : Point {return when {//' build flag.and(FLAG_TOP)! = 0 -> { val y = -windowInfo? .height.value() //' windowInfo '; //' windowInfo '; .width. Value ()) Point(x, y)} //' flag.and(FLAG_BOTTOM)! = 0 -> { val y = screenHeight val x = getValueByGravity(flag, screenWidth, windowInfo? .width. Value ()) Point(x, y)} //' flag. And (FLAG_LEFT)! = 0 -> { val x = -windowInfo? .width.value() val y = getValueByGravity(flag, screenHeight, windowInfo? .height.value() Point(x, y)} //' flag. = 0 -> { val x = screenWidth val y = getValueByGravity(flag, screenHeight, windowInfo? .height.value()) Point(x, y) } else -> Point(0, 0) } } }Copy the code

Gravity gravity gravity gravity gravity gravity gravity

In order to make the floating window move into the screen, its initial position is placed outside the screen and close to the edge of the screen. For example, the bottom edge of the top float window is close to the top edge of the screen, so the top left corner of the float window is y = – float height

The parse gravity logic is written in getValueByGravity() :

private fun getValueByGravity(flag: Int, total: Int, actual: Int): Int = when { flag.and(FLAG_START) ! = 0 -> 0 flag.and(FLAG_MID) ! = 0 -> (total - actual) / 2 flag.and(FLAG_END) ! = 0 -> (total - actual) else -> 0 }Copy the code

Where total indicates the side screen width (height), actual indicates the corresponding floating window width (height)

In the animation

After the initial position of the floating window is placed outside the screen and close to the screen, it only needs to set another displacement animation to achieve the effect of moving into the screen:

object FloatWindow : View.OnTouchListener { fun show( context: Context, tag: String, windowInfo: WindowInfo? WindowInfoMap [tag], flag: Int, //' windowInfoMap ', onAnimateWindow: ((WindowInfo?) -> Unit)? ) { getShowPoint(flag, windowInfo).let { show(context, tag, windowInfo, it.x, it.y, False)} //' Animate window shift at end of current message queue 'windowInfo? .view? .post { onAnimateWindow? .invoke(windowInfo) } } }Copy the code

The overloaded show() function hands over the implementation of the animation to the business layer, and the animation is timed at the end of the message queue to ensure that the animation is executed after the window is displayed.

The business interface now displays the top drop window like this:

var handler = Handler(Looper.getMainLooper()) val view = LayoutInflater.from(this).inflate(R.layout.gravity_vertical_window, WindowInfo = floatWindow.windowinfo (view). Apply {width = dimensionUtil.dp2px (300.0) height = Dimensionutil.dp2px (80.0)} //' Default window. show(this, "top", windowInfo) FLAG_TOP or FLAG_MID) { info -> val anim = animSet { anim { values = intArrayOf(info.layoutParams?.y ?: 0, 0) interpolator = LinearOutSlowInInterpolator() duration = 250L action = { value -> FloatWindow.updateWindowView(y = Value as Int)}} start()} //' handler. PostDelayed ({im.reverse()}, 1500)}Copy the code

AnimSet and Anim are custom DSLS that simplify the construction of animation code, using Kotlin syntax sugar analysis, you can click Kotlin advanced: Animation code is too ugly, save with DSL animation library, write code like talking yo! .

Further reloading provides default animation

Leaving the details of building floating window entry animations to the business interface adds flexibility, but also complexity to the business code. It would be better if you could provide the default animation, reload show() :

Object FloatWindow: view. OnTouchListener {//' Provides default animation for floating window display overloaded functions' fun show(context: context, tag: String, windowInfo: WindowInfo? If windowInfoMap[tag] = windowInfoMap[tag], flag: Int, offset: Int = 0, Long = 1500L ) { getShowPoint(flag, windowInfo, offset).let { show(context, tag, windowInfo, it.x, it.y, false) } windowInfo? .view? .post {//' Build floating window appearance animation 'getShowAnim(flag, windowInfo, duration)? .also {anim -> anim.start() //' delay hide float window 'handler.postdelayed ({anim.reverse() //' delay hide float window animation when finished, Dismiss floating window 'im.onEnd = {dismiss(windowInfo)}}, stayTime)}}}}Copy the code

GetShowAnim () builds the corresponding exit animation by parsing flag:

object FloatWindow : View.OnTouchListener { private fun getShowAnim(flag: Int, windowInfo: WindowInfo? , duration: Long): AnimSet? = when {//' build top-down animation 'flag.and(FLAG_TOP)! = 0 -> { animSet { anim { values = intArrayOf(windowInfo? .layoutParams? .y.value(), 0) this.duration = duration interpolator = LinearOutSlowInInterpolator() action = { value -> updateWindowView(y = value As Int)}}}} //' build bottom-up animation 'flag.and(FLAG_BOTTOM)! = 0 -> { animSet { anim { values = intArrayOf(windowInfo? .layoutParams? .y.value(), windowInfo? .layoutParams? .y.value() - windowInfo? .height.value()) this.duration = duration interpolator = LinearOutSlowInInterpolator() action = { value -> UpdateWindowView (y = value as Int)}}}} //' build left-to-right animation 'flag.and(FLAG_LEFT)! = 0 -> { animSet { anim { values = intArrayOf(windowInfo? .layoutParams? .x.value(), 0) this.duration = duration interpolator = LinearOutSlowInInterpolator() action = { value -> updateWindowView(x = value As Int)}}}} //' build right to left animation 'flag.and(FLAG_RIGHT)! = 0 -> { animSet { anim { values = intArrayOf(windowInfo? .layoutParams? .x.value(), windowInfo? .layoutParams? .x.value() - windowInfo? .layoutParams? .width.value()) this.duration = duration interpolator = LinearOutSlowInInterpolator() action = { value -> updateWindowView(x = value as Int) } } } } else -> null } }Copy the code

Although there are 12 common positions, there are only 4 directions for floating window animation, namely, top down, bottom up, left to right and right to left.

Slide up to hide floating window

To hide uninterested notifications, play the animation backwards while listening to the fling gesture:

object FloatWindow : View.OnTouchListener { private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener()) private var inAndOutAnim: Anim? = null 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 onFling(E1: MotionEvent, E2: MotionEvent, velocityX: Float, velocityY: Float) Float): Boolean {//' reverse entry animation 'inAndOutAnim? .let { anim -> anim.reverse() anim.onEnd = { dismiss(windowInfo) } return true } return false } } }Copy the code

InAndOutAnim should be assigned in two overloaded show() functions, so modify show() as follows:

object FloatWindow : View.OnTouchListener { fun show( context: Context, tag: String, windowInfo: WindowInfo? = windowInfoMap[tag], flag: Int, offset: Int = 0, duration: Long = 250L, stayTime: Long = 1500L ) { getShowPoint(flag, windowInfo, offset).let { show(context, tag, windowInfo, it.x, it.y, false) } windowInfo? .view? .post {//' Build default entry animation to assign inAndOutAnim 'inAndOutAnim = getShowAnim(flag, windowInfo, duration)? .also { anim -> anim.start() handler.postDelayed({ anim.reverse() anim.onEnd = { dismiss(windowInfo) } }, stayTime) } } } fun show( context: Context, tag: String, windowInfo: WindowInfo? If ((WindowInfo) -> onAnimateWindow: ((WindowInfo) -> onAnimateWindow)?) { getShowPoint(flag, windowInfo, offset).let { show(context, tag, windowInfo, it.x, it.y, False)} //' Business interface builds the entry animation as the return value of lambda and assigns it to inAndOutAnim' windowInfo? .view? .post { inAndOutAnim = onAnimateWindow? .invoke(windowInfo)}} //' the business interface displays a sunk notification 'val view = LayoutInflater.from(this).inflate(R.layout.gravity_vertical_window, WindowInfo = floatWindow.windowinfo (view). Apply {width = DimensionUtil.dp2px(300.0) height = Dimensionutil.dp2px (80.0)} FloatWindow. Show (this, "top", windowInfo, FLAG_TOP or FLAG_MID) { info -> val anim = animSet { anim { values = intArrayOf(info.layoutParams?.y ?: 0, 0) interpolator = LinearOutSlowInInterpolator() duration = 250L action = { value -> FloatWindow.updateWindowView(y = Value as Int)}} start()} handler.postdelayed ({im.reverse()}, 1500) //' build animation instance as lambda value 'anim}Copy the code

Global floating window

The notification type float window is different from other float Windows in that it is global and requires the float window to be displayed continuously when switching activities. Simply apply for a permission statically and change the window type to achieve:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
Copy the code

Add this permission to androidmanifest.xml, and then modify the show() function to add global arguments:

object FloatWindow : View.OnTouchListener { fun show( context: Context, tag: String, windowInfo: WindowInfo? = windowInfoMap[tag], x: Int = windowInfo? .layoutParams? .x.value(), y: Int = windowInfo? .layoutParams? .y.value(), dragEnable: Boolean = false, //' Is it a global float window 'overall: Boolean = false) {... WindowInfo. LayoutParams = createLayoutParam(x, y, overall) if (! windowInfo.hasParent().value()) { val windowManager = this.context? .getSystemService(Context.WINDOW_SERVICE) as? WindowManager prepareScreenDimension(windowManager) windowManager? .addView(windowInfo.view, windowInfo.layoutParams) updateWindowViewSize() onWindowShow? .invoke() } } private fun createLayoutParam(x: Int, y: Int, overall: Boolean): WindowManager.LayoutParams { if (context == null) { return WindowManager.LayoutParams() } return WindowManager. LayoutParams (). The apply {/ / 'to the window of the floating window to specify different types' global type = the if (overall) {if (Build) VERSION) SDK_INT > = Build.VERSION_CODES.O) { WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } else { WindowManager.LayoutParams.TYPE_SYSTEM_ALERT } } else { WindowManager.LayoutParams.TYPE_APPLICATION } format = PixelFormat.TRANSLUCENT flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or WindowManager.LayoutParams.FLAG_DIM_BEHIND dimAmount = 0f this.gravity = Gravity.START or Gravity.TOP width = windowInfo? .width.value() height = windowInfo? .height.value() this.x = x this.y = y } } }Copy the code

When the Window type is set to TYPE_APPLICATION_OVERLAY or TYPE_SYSTEM_ALERT, the Window is not affiliated to an Activity. This gives you a global representation.

talk is cheap, show me the code

The full code can be found at the link above.