Android应用内悬浮窗完美实现-无需权限

android实现悬浮窗有很多方式,经过反复测试,研究windowManager,view的绘制,布局等原理,实现了一个优雅地展示,并且可以流畅缩放悬的浮窗,无需用户授权。直接贴关键代码,以直播间,悬浮窗播放直播为例。

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
/**
* 初始化悬浮窗
*/
private void initFloatWindow(final Context context) {
floatViewParams = initFloatViewParams(context);
if (FLOAT_WINDOW_TYPE == FLOAT_WINDOW_TYPE_ROOT_VIEW) {
initCommonFloatView(context);
} else if (FLOAT_WINDOW_TYPE == FLOAT_WINDOW_TYPE_WM) {
initSystemWindow(context);
}
isFloatWindowShowing = true;
}
/**
* 直接在activity根布局添加悬浮窗
*
* @param context
*/
private void initCommonFloatView(Context context) {
floatView = new NonoFloatView(context, floatViewParams, renderCallback);
View rootView = activity.getWindow().getDecorView().getRootView();
contentView = (FrameLayout) rootView.findViewById(android.R.id.content);
contentView.addView((View) floatView);
}
/**
* 利用系统弹窗实现悬浮窗
*
* @param context
*/
private void initSystemWindow(Context context) {
windowManager = SystemUtils.getWindowManager(context);
WindowManager.LayoutParams wmParams = new WindowManager.LayoutParams();
wmParams.packageName = context.getPackageName();
wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_SCALED
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;//TYPE_SYSTEM_ALERT
wmParams.format = PixelFormat.RGBA_8888;//透明
wmParams.gravity = Gravity.START | Gravity.TOP;
wmParams.width = floatViewParams.width;
wmParams.height = floatViewParams.height;
wmParams.x = floatViewParams.x;
wmParams.y = floatViewParams.y;
floatView = new FloatWindowView(context, floatViewParams, wmParams, renderCallback);
windowManager.addView((View) floatView, wmParams);
}
/**
* 初始化窗口参数
*
* @param context
* @return
*/
private FloatViewParams initFloatViewParams(Context context) {
FloatViewParams params = new FloatViewParams();
int screenWidth = SystemUtils.getScreenWidth(context);
int screenHeight = SystemUtils.getScreenHeight(context, false);
//根据播放器实际宽高和设计稿尺寸比例适应。191 340 114
int marginBottom = SystemUtils.dip2px(context, 64);
int videoWidth = livePlayerWrapper.getVideoWidth();
int videoHeight = livePlayerWrapper.getVideoHeight();
int videoViewMargin = SystemUtils.dip2px(context, 15);
int width = 0;
if (videoWidth <= videoHeight) {//竖屏比例
width = (int) (screenWidth * 1.0f * 190 / 750) + videoViewMargin;
} else {//横屏比例
width = (int) (screenWidth * 1.0f / 3) + videoViewMargin;
}
float ratio = 1.0f * videoHeight / videoWidth;
int height = (int) (width * ratio);
//如果上次的位置不为null,则用上次的位置
FloatViewParams lastParams = livePlayerWrapper.getLastParams();
if (lastParams != null) {
params.width = lastParams.width;
params.height = lastParams.height;
params.x = lastParams.x;
params.y = lastParams.y;
params.contentWidth = lastParams.contentWidth;
} else {
params.width = width;
params.height = height;
params.x = screenWidth - width;
params.y = screenHeight - height - marginBottom;
params.contentWidth = width;
}
params.screenWidth = screenWidth;
params.screenHeight = screenHeight;
params.videoViewMargin = videoViewMargin;
params.mMaxWidth = screenWidth / 2 + videoViewMargin;
params.mMinWidth = width;
params.mRatio = ratio;
//Logger.d(TAG, "dq initFloatViewParams x=" + params.x + ",y=" + params.y + ",ratio=" + ratio);
return params;
}

下面是自定义view,FloatView是一个组合控件,实现了悬浮窗view的点击,移动和缩放功能。

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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
/**
* NonoFloatView:悬浮窗控件V2,普通的实现
*
* @author Nonolive-杜乾 Created on 2017/12/12 - 17:16.
* E-mail:dusan.du@nonolive.com
*/
public class NonoFloatView extends FrameLayout implements IFloatView {
private static final String TAG = NonoFloatView.class.getSimpleName();
private float xInView;
private float yInView;
private float xInScreen;
private float yInScreen;
private float xDownInScreen;
private float yDownInScreen;
private Context context;
private TextView tv_player_status;
private VideoTextureRenderView videoTextureRenderView;
private VideoTextureRenderView.IRenderCallback renderCallback;
private RelativeLayout videoViewWrap;
private RelativeLayout content_wrap;
private ImageView iv_live_cover;
private ImageView iv_zoom_btn;
private FloatViewParams params = null;
private FloatViewListener listener;
private int statusBarHeight = 0;
private int screenWidth;
private int screenHeight;
private int mMinWidth;//初始宽度
private int mMaxWidth;//视频最大宽度
private float mRatio = 1.77f;//窗口高/宽比
private int videoViewMargin;
private View floatView;
public NonoFloatView(Context context) {
super(context);
init();
}
public NonoFloatView(@NonNull Context context, FloatViewParams params, VideoTextureRenderView.IRenderCallback callback) {
super(context);
this.params = params;
this.renderCallback = callback;
init();
}
private void init() {
initData();
initView();
}
private void initView() {
LayoutInflater inflater = LayoutInflater.from(getContext());
floatView = inflater.inflate(R.layout.nn_liveroom_video_float_window, null);
content_wrap = (RelativeLayout) floatView.findViewById(R.id.content_wrap);
videoViewWrap = (RelativeLayout) floatView.findViewById(R.id.videoViewWrap);
tv_player_status = (TextView) floatView.findViewById(R.id.tv_player_status);
iv_live_cover = (ImageView) floatView.findViewById(R.id.iv_live_cover);
iv_zoom_btn = (ImageView) floatView.findViewById(R.id.iv_zoom_btn);
iv_zoom_btn.setOnTouchListener(onZoomBtnTouchListener);
content_wrap.setOnTouchListener(onMovingTouchListener);
content_wrap.addOnLayoutChangeListener(onLayoutChangeListener);
ImageView iv_close_window = (ImageView) floatView.findViewById(R.id.iv_close_window);
iv_close_window.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (null != NonoFloatView.this.listener) {
NonoFloatView.this.listener.onClose();//关闭
}
}
});
videoTextureRenderView = (VideoTextureRenderView) floatView.findViewById(R.id.float_textureView);
videoTextureRenderView.addRenderCallback(renderCallback);
int lastViewWidth = params.contentWidth;
int lastViewHeight = (int) (lastViewWidth * mRatio);
updateViewLayoutParams(lastViewWidth, lastViewHeight);
addView(floatView);
}
private void initData() {
context = getContext();
statusBarHeight = getStatusBarHeight();
if (params != null) {
screenWidth = params.screenWidth;
screenHeight = params.screenHeight - statusBarHeight;//要去掉状态栏高度
videoViewMargin = params.videoViewMargin;
mMaxWidth = params.mMaxWidth;
mMinWidth = params.mMinWidth;
mRatio = params.mRatio;
}
oldX = params.x;
oldY = params.y;
mRight = params.x + params.width;
mBottom = params.y + params.height;
//Logger.d(TAG, " dq mRight=" + mRight + "/" + mBottom + ",rangeWidth=" + mMinWidth + "-" + mMaxWidth + ",mRatio=" + mRatio);
}
private void updateViewLayoutParams(int width, int height) {
if (content_wrap != null) {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) content_wrap.getLayoutParams();
layoutParams.height = height;
layoutParams.width = width;
content_wrap.setLayoutParams(layoutParams);
params.width = width;
params.height = height;
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!isRestorePosition) {
content_wrap.layout(oldX, oldY, oldX + params.width, oldY + params.height);
isRestorePosition = true;
}
}
private boolean isRestorePosition = false;//是否恢复上次页面位置
private int oldX = 0;
private int oldY = 0;
// 监听layout变化
private final OnLayoutChangeListener onLayoutChangeListener = new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (right != mRight || bottom != mBottom) {
//Logger.d(TAG, "dq onLayoutChange111 left=" + left + ",top=" + top + ",right=" + right + ",bottom=" + bottom);
int width = content_wrap.getWidth();
int height = content_wrap.getHeight();
//防止拖出屏幕外部,顶部和右下角处理
int l = mRight - width;
int t = mBottom - height;
int r = mRight;
int b = mBottom;
if (l < -videoViewMargin) {//0
l = -videoViewMargin;
r = l + width;
}
if (t < -videoViewMargin) {//0
t = -videoViewMargin;
b = t + height;
}
content_wrap.layout(l, t, r, b);
params.x = l;
params.y = t;
}
}
};
private int mRight = 0;
private int mBottom = 0;
private final OnTouchListener onZoomBtnTouchListener = new OnTouchListener() {
float lastX = 0;
float lastY = 0;
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
isDragged = true;
lastX = event.getRawX();
lastY = event.getRawY();
//记录右下角定点的位置,right 和 bottom
mRight = content_wrap.getRight();
mBottom = content_wrap.getBottom();
break;
case MotionEvent.ACTION_MOVE:
showZoomView();
handleMoveEvent(event);
break;
case MotionEvent.ACTION_UP:
if (listener != null) {
listener.onDragged();
}
displayZoomViewDelay();
isDragged = false;
break;
default:
break;
}
return true;
}
private void handleMoveEvent(MotionEvent event) {
isDragged = true;
float moveX = event.getRawX();
float moveY = event.getRawY();
float dx = moveX - lastX;
float dy = moveY - lastY;
double distance = Math.sqrt(dx * dx + dy * dy);
if (distance >= 5) {//控制刷新频率
//已经是最大或者最小不缩放
int contentWidth = content_wrap.getWidth();
if (moveY > lastY && moveX > lastX) {
if (contentWidth == mMinWidth) {//最小了,不能再小了
return;
}
distance = -distance;//缩小
} else {
if (contentWidth == mMaxWidth) {
return;
}
}
int changedWidth = (int) (distance * Math.cos(45));//粗略计算
//调节内部view大小
updateContentViewSize(changedWidth);
}
lastX = moveX;
lastY = moveY;
}
};
public int getContentViewWidth() {
return content_wrap != null ? content_wrap.getWidth() : mMinWidth;
}
/**
* 更新内部view的大小
*
* @param width 传入变化的宽度
*/
private void updateContentViewSize(int width) {
int currentWidth = content_wrap.getWidth();
int newWidth = currentWidth + width;
newWidth = checkWidth(newWidth);
int height = (int) (newWidth * mRatio);
//params.x = params.x - width / 2;
//params.y = params.y - width / 2;
//调整视频view的大小
updateViewLayoutParams(newWidth, height);
}
/**
* 修正大小,限制最大和最小值
*
* @param width
* @return
*/
private int checkWidth(int width) {
if (width > mMaxWidth) {
width = mMaxWidth;
}
if (width < mMinWidth) {
width = mMinWidth;
}
return width;
}
private boolean isMoving = false;
private final OnTouchListener onMovingTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return onTouchEvent2(event);
}
};
//@Override
public boolean onTouchEvent2(MotionEvent event) {
if (isDragged) {
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
showZoomView();
isMoving = false;
xInView = event.getX();
yInView = event.getY();
Rect rect = new Rect();
floatView.getGlobalVisibleRect(rect);
if (!rect.contains((int) xInView, (int) yInView)) {//不在移动的view内,不处理
return false;
}
xDownInScreen = event.getRawX();
yDownInScreen = event.getRawY();
xInScreen = xDownInScreen;
yInScreen = yDownInScreen;
break;
case MotionEvent.ACTION_MOVE:
showZoomView();
// 手指移动的时候更新小悬浮窗的位置
xInScreen = event.getRawX();
yInScreen = event.getRawY();
if (!isMoving) {
isMoving = !isClickedEvent();
} else {
updateViewPosition();
}
break;
case MotionEvent.ACTION_UP:
if (isClickedEvent()) {
if (null != listener) {
listener.onClick();
}
} else {
if (null != listener) {
listener.onMoved();
}
}
//updateEditStatus();
displayZoomViewDelay();
isMoving = false;
break;
default:
break;
}
return true;
}
/**
* 是否为点击事件
*
* @return
*/
private boolean isClickedEvent() {
int scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();// - 10;
if (Math.abs(xDownInScreen - xInScreen) <= scaledTouchSlop
&& Math.abs(yDownInScreen - yInScreen) <= scaledTouchSlop) {
return true;
}
return false;
}
/**
* 更新悬浮窗位置
*/
private synchronized void updateViewPosition() {
int x = (int) (xInScreen - xInView);
int y = (int) (yInScreen - yInView);
//边界处理
if (x <= -videoViewMargin) {
x = -videoViewMargin;
}
if (y <= -videoViewMargin) {
y = -videoViewMargin;
}
int dWidth = screenWidth - content_wrap.getWidth();
if (x >= dWidth) {
x = dWidth;
}
int dHeight = screenHeight - content_wrap.getHeight();
if (y >= dHeight) {
y = dHeight;
}
if (x >= dWidth) {
x = dWidth - 1;
}
//Logger.d(TAG, "dq updateViewPosition x=" + x + ",y=" + y);
reLayoutContentView(x, y);
}
/**
* 重新布局
*
* @param x
* @param y
*/
private void reLayoutContentView(int x, int y) {
//更新起点
params.x = x;
params.y = y;
mRight = x + content_wrap.getWidth();
mBottom = y + content_wrap.getHeight();
content_wrap.layout(x, y, mRight, mBottom);
}
private boolean isDragged = false;//是否正在拖拽中
private boolean isEdit = false;//是否进入编辑状态
/**
* 显示拖拽缩放按钮
*/
private void showZoomView() {
if (!isEdit) {
iv_zoom_btn.setVisibility(VISIBLE);
videoViewWrap.setBackgroundColor(getResources().getColor(R.color.float_window_bg_border_edit));
isEdit = true;
}
}
/**
* 隐藏缩放按钮
*/
private void displayZoomView() {
isEdit = false;
iv_zoom_btn.setVisibility(GONE);
videoViewWrap.setBackgroundColor(getResources().getColor(R.color.float_window_bg_border_normal));
}
/**
* 处理缩放按钮隐藏时视频的margin
*/
private void handleMarginStatus() {
boolean isLeft = params.x <= -videoViewMargin;
boolean isTop = params.y <= -videoViewMargin;
// 贴边时设置视频margin
if (isLeft && isTop) {
updateVideoMargin(0, 0, videoViewMargin, videoViewMargin);
} else if (isLeft) {
updateVideoMargin(0, videoViewMargin, videoViewMargin, 0);
} else if (isTop) {
updateVideoMargin(videoViewMargin, 0, 0, videoViewMargin);
}
}
/**
* 调整视频view的边距
*/
private void updateVideoMargin(int left, int top, int right, int bottom) {
if (videoViewWrap != null) {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) videoViewWrap.getLayoutParams();
layoutParams.setMargins(left, top, right, bottom);
videoViewWrap.setLayoutParams(layoutParams);
}
}
private void displayZoomViewDelay() {
removeCallbacks(dispalyZoomBtnRunnable);
postDelayed(dispalyZoomBtnRunnable, 2000);
}
private final Runnable dispalyZoomBtnRunnable = new Runnable() {
@Override
public void run() {
displayZoomView();
}
};
private int getStatusBarHeight() {
int statusBarHeight = SystemUtils.getStatusBarHeightByReflect(context);
if (statusBarHeight == 0) {
statusBarHeight = SystemUtils.dip2px(context, 30);
}
return statusBarHeight;
}
//略

下面是自定义view的xml布局。

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:id="@+id/fix_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/content_wrap"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true">
<!--视频主体-->
<RelativeLayout
android:id="@+id/videoViewWrap"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="15dp"
android:layout_marginTop="15dp"
android:background="@color/float_window_bg_border_normal"
android:padding="1dp">
<tv.danmaku.ijk.media.nono.widget.VideoTextureRenderView
android:id="@+id/float_textureView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/iv_close_window"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:paddingBottom="15dp"
android:paddingLeft="15dp"
android:paddingRight="5dp"
android:paddingTop="5dp"
android:scaleType="centerInside"
android:src="@drawable/nn_room_btn_close_new"
/>
<ImageView
android:id="@+id/iv_live_cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.5"
android:background="@color/alpha_80_black"
android:scaleType="centerCrop"
android:visibility="gone"/>
<TextView
android:id="@+id/tv_player_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:text=""
android:textColor="@color/default_theme_blue_007"
android:textSize="@dimen/text_size_14"
android:textStyle="bold"
android:visibility="gone"/>
</RelativeLayout>
<!--缩放按钮-->
<ImageView
android:id="@+id/iv_zoom_btn"
android:layout_width="60dp"
android:layout_height="60dp"
android:paddingBottom="20dp"
android:paddingRight="20dp"
android:scaleType="centerInside"
android:src="@drawable/nn_paster_btn_scale"
android:visibility="gone"
/>
</RelativeLayout>
</RelativeLayout>

详细代码,请移步我的Github,请star一下表示支持噢:
Android-FloatWindow


杜乾,Dusan,duqian2010@gmail.com,QQ:291902259

微信公众号:OpenDeveloper

分享不仅限于Android,Web 开发,做开放的开发者。

Blog:http://blog.csdn.net/dzsw0117

Github:duqian291902259