fix
lmw
2025-03-04 449bdb5d2b5bf7b272ca5cda4c066f9a65040064
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
package com.lzf.easyfloat.core
 
import android.animation.Animator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Rect
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.WindowManager.LayoutParams
import com.lzf.easyfloat.data.FloatConfig
import com.lzf.easyfloat.enums.ShowPattern
import com.lzf.easyfloat.enums.SidePattern
import com.lzf.easyfloat.utils.DisplayUtils
import kotlin.math.max
import kotlin.math.min
 
/**
 * @author: liuzhenfeng
 * @function: 根据吸附模式,实现相应的拖拽效果
 * @date: 2019-07-05  14:24
 */
internal class TouchUtils(val context: Context, val config: FloatConfig) {
 
    // 窗口所在的矩形
    private var parentRect: Rect = Rect()
 
    // 悬浮的父布局高度、宽度
    private var parentHeight = 0
    private var parentWidth = 0
 
    // 四周坐标边界值
    private var leftBorder = 0
    private var topBorder = 0
    private var rightBorder = 0
    private var bottomBorder = 0
 
    // 起点坐标
    private var lastX = 0f
    private var lastY = 0f
 
    // 浮窗各边距离父布局的距离
    private var leftDistance = 0
    private var rightDistance = 0
    private var topDistance = 0
    private var bottomDistance = 0
 
    // x轴、y轴的最小距离值
    private var minX = 0
    private var minY = 0
    private val location = IntArray(2)
    private var statusBarHeight = 0
 
    // 屏幕可用高度 - 浮窗自身高度 的剩余高度
    private var emptyHeight = 0
 
    /**
     * 根据吸附模式,实现相应的拖拽效果
     */
    fun updateFloat(
        view: View,
        event: MotionEvent,
        windowManager: WindowManager,
        params: LayoutParams
    ) {
        config.callbacks?.touchEvent(view, event)
        config.floatCallbacks?.builder?.touchEvent?.invoke(view, event)
        // 不可拖拽、或者正在执行动画,不做处理
        if (!config.dragEnable || config.isAnim) {
            config.isDrag = false
            return
        }
 
        when (event.action and MotionEvent.ACTION_MASK) {
            MotionEvent.ACTION_DOWN -> {
                config.isDrag = false
                // 记录触摸点的位置
                lastX = event.rawX
                lastY = event.rawY
                // 初始化一些边界数据
                initBoarderValue(view, params)
            }
 
            MotionEvent.ACTION_MOVE -> {
                // 过滤边界值之外的拖拽
                if (event.rawX < leftBorder || event.rawX > rightBorder + view.width
                    || event.rawY < topBorder || event.rawY > bottomBorder + view.height
                ) return
 
                // 移动值 = 本次触摸值 - 上次触摸值
                val dx = event.rawX - lastX
                val dy = event.rawY - lastY
                // 忽略过小的移动,防止点击无效
                if (!config.isDrag && dx * dx + dy * dy < 81) return
                config.isDrag = true
 
                var x = params.x + dx.toInt()
                var y = params.y + dy.toInt()
                // 检测浮窗是否到达边缘
                x = when {
                    x < leftBorder -> leftBorder
                    x > rightBorder -> rightBorder
                    else -> x
                }
 
                if (config.showPattern == ShowPattern.CURRENT_ACTIVITY) {
                    // 单页面浮窗,设置状态栏不沉浸时,最小高度为状态栏高度
                    if (y < statusBarHeight(view) && !config.immersionStatusBar) y =
                        statusBarHeight(view)
                }
 
                y = when {
                    y < topBorder -> topBorder
                    // 状态栏沉浸时,最小高度为-statusBarHeight,反之最小高度为0
                    y < 0 -> if (config.immersionStatusBar) {
                        if (y < -statusBarHeight) -statusBarHeight else y
                    } else 0
                    y > bottomBorder -> bottomBorder
                    else -> y
                }
 
                when (config.sidePattern) {
                    SidePattern.LEFT -> x = 0
                    SidePattern.RIGHT -> x = parentWidth - view.width
                    SidePattern.TOP -> y = 0
                    SidePattern.BOTTOM -> y = emptyHeight
 
                    SidePattern.AUTO_HORIZONTAL ->
                        x = if (event.rawX * 2 > parentWidth) parentWidth - view.width else 0
 
                    SidePattern.AUTO_VERTICAL ->
                        y = if ((event.rawY - parentRect.top) * 2 > parentHeight)
                            parentHeight - view.height else 0
 
                    SidePattern.AUTO_SIDE -> {
                        leftDistance = event.rawX.toInt()
                        rightDistance = parentWidth - event.rawX.toInt()
                        topDistance = event.rawY.toInt() - parentRect.top
                        bottomDistance = parentHeight + parentRect.top - event.rawY.toInt()
 
                        minX = min(leftDistance, rightDistance)
                        minY = min(topDistance, bottomDistance)
                        if (minX < minY) {
                            x = if (leftDistance == minX) 0 else parentWidth - view.width
                        } else {
                            y = if (topDistance == minY) 0 else emptyHeight
                        }
                    }
                    else -> {
                    }
                }
 
                // 重新设置坐标信息
                params.x = x
                params.y = y
                windowManager.updateViewLayout(view, params)
                config.callbacks?.drag(view, event)
                config.floatCallbacks?.builder?.drag?.invoke(view, event)
                // 更新上次触摸点的数据
                lastX = event.rawX
                lastY = event.rawY
            }
 
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                if (!config.isDrag) return
                // 回调拖拽事件的ACTION_UP
                config.callbacks?.drag(view, event)
                config.floatCallbacks?.builder?.drag?.invoke(view, event)
                when (config.sidePattern) {
                    SidePattern.RESULT_LEFT,
                    SidePattern.RESULT_RIGHT,
                    SidePattern.RESULT_TOP,
                    SidePattern.RESULT_BOTTOM,
                    SidePattern.RESULT_HORIZONTAL,
                    SidePattern.RESULT_VERTICAL,
                    SidePattern.RESULT_SIDE -> sideAnim(view, params, windowManager)
                    else -> {
                        config.callbacks?.dragEnd(view)
                        config.floatCallbacks?.builder?.dragEnd?.invoke(view)
                    }
                }
            }
 
            else -> return
        }
    }
 
    /**
     * 根据吸附类别,更新浮窗位置
     */
    fun updateFloat(
        view: View,
        params: LayoutParams,
        windowManager: WindowManager
    ) {
        initBoarderValue(view, params)
        sideAnim(view, params, windowManager)
    }
 
    /**
     * 初始化边界值等数据
     */
    private fun initBoarderValue(view: View, params: LayoutParams) {
        // 屏幕宽高需要每次获取,可能会有屏幕旋转、虚拟导航栏的状态变化
        parentWidth = DisplayUtils.getScreenWidth(context)
        parentHeight = config.displayHeight.getDisplayRealHeight(context)
        // 获取在整个屏幕内的绝对坐标
        view.getLocationOnScreen(location)
        // 通过绝对高度和相对高度比较,判断包含顶部状态栏
        statusBarHeight = if (location[1] > params.y) statusBarHeight(view) else 0
        emptyHeight = parentHeight - view.height - statusBarHeight
 
        leftBorder = max(0, config.leftBorder)
        rightBorder = min(parentWidth, config.rightBorder) - view.width
        topBorder = if (config.showPattern == ShowPattern.CURRENT_ACTIVITY) {
            // 单页面浮窗,坐标屏幕顶部计算
            if (config.immersionStatusBar) config.topBorder
            else config.topBorder + statusBarHeight(view)
        } else {
            // 系统浮窗,坐标从状态栏底部开始,沉浸时坐标为负
            if (config.immersionStatusBar) config.topBorder - statusBarHeight(view) else config.topBorder
        }
        bottomBorder = if (config.showPattern == ShowPattern.CURRENT_ACTIVITY) {
            // 单页面浮窗,坐标屏幕顶部计算
            if (config.immersionStatusBar)
                min(emptyHeight, config.bottomBorder - view.height)
            else
                min(emptyHeight, config.bottomBorder + statusBarHeight(view) - view.height)
        } else {
            // 系统浮窗,坐标从状态栏底部开始,沉浸时坐标为负
            if (config.immersionStatusBar)
                min(emptyHeight, config.bottomBorder - statusBarHeight(view) - view.height)
            else
                min(emptyHeight, config.bottomBorder - view.height)
        }
    }
 
    private fun sideAnim(
        view: View,
        params: LayoutParams,
        windowManager: WindowManager
    ) {
        initDistanceValue(params)
        val isX: Boolean
        val end = when (config.sidePattern) {
            SidePattern.RESULT_LEFT -> {
                isX = true
                leftBorder
            }
            SidePattern.RESULT_RIGHT -> {
                isX = true
                params.x + rightDistance
            }
            SidePattern.RESULT_HORIZONTAL -> {
                isX = true
                if (leftDistance < rightDistance) leftBorder else params.x + rightDistance
            }
 
            SidePattern.RESULT_TOP -> {
                isX = false
                topBorder
            }
            SidePattern.RESULT_BOTTOM -> {
                isX = false
                // 不要轻易使用此相关模式,需要考虑虚拟导航栏的情况
                bottomBorder
            }
            SidePattern.RESULT_VERTICAL -> {
                isX = false
                if (topDistance < bottomDistance) topBorder else bottomBorder
            }
 
            SidePattern.RESULT_SIDE -> {
                if (minX < minY) {
                    isX = true
                    if (leftDistance < rightDistance) leftBorder else params.x + rightDistance
                } else {
                    isX = false
                    if (topDistance < bottomDistance) topBorder else bottomBorder
                }
            }
            else -> return
        }
 
        val animator = ValueAnimator.ofInt(if (isX) params.x else params.y, end)
        animator.addUpdateListener {
            try {
                if (isX) params.x = it.animatedValue as Int else params.y = it.animatedValue as Int
                // 极端情况,还没吸附就调用了关闭浮窗,会导致吸附闪退
                windowManager.updateViewLayout(view, params)
            } catch (e: Exception) {
                animator.cancel()
            }
        }
        animator.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {}
 
            override fun onAnimationEnd(animation: Animator?) {
                dragEnd(view)
            }
 
            override fun onAnimationCancel(animation: Animator?) {
                dragEnd(view)
            }
 
            override fun onAnimationStart(animation: Animator?) {
                config.isAnim = true
            }
        })
        animator.start()
    }
 
    private fun dragEnd(view: View) {
        config.isAnim = false
        config.callbacks?.dragEnd(view)
        config.floatCallbacks?.builder?.dragEnd?.invoke(view)
    }
 
    /**
     * 计算一些边界距离数据
     */
    private fun initDistanceValue(params: LayoutParams) {
        leftDistance = params.x - leftBorder
        rightDistance = rightBorder - params.x
        topDistance = params.y - topBorder
        bottomDistance = bottomBorder - params.y
 
        minX = min(leftDistance, rightDistance)
        minY = min(topDistance, bottomDistance)
    }
 
    private fun statusBarHeight(view: View) = DisplayUtils.statusBarHeight(view)
 
}