Browse Source
实现Android平台的录音悬浮窗服务,支持显示录音时长、类型和暂停状态 添加悬浮窗操作事件通道,支持暂停/继续、打开会议记录和关闭悬浮窗功能 在会议记录控制器中集成悬浮窗逻辑weicu
8 changed files with 620 additions and 3 deletions
@ -1,3 +1,11 @@ |
|||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
package="com.example.native_plugin"> |
package="com.example.native_plugin"> |
||||
|
|
||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> |
||||
|
|
||||
|
<application> |
||||
|
<service |
||||
|
android:name=".RecordingFloatingService" |
||||
|
android:exported="false" /> |
||||
|
</application> |
||||
</manifest> |
</manifest> |
||||
|
|||||
@ -0,0 +1,292 @@ |
|||||
|
package com.example.native_plugin; |
||||
|
|
||||
|
import android.app.Service; |
||||
|
import android.content.Intent; |
||||
|
import android.graphics.Color; |
||||
|
import android.graphics.PixelFormat; |
||||
|
import android.graphics.drawable.GradientDrawable; |
||||
|
import android.os.Build; |
||||
|
import android.os.IBinder; |
||||
|
import android.util.TypedValue; |
||||
|
import android.view.Gravity; |
||||
|
import android.view.MotionEvent; |
||||
|
import android.view.View; |
||||
|
import android.view.ViewConfiguration; |
||||
|
import android.view.WindowManager; |
||||
|
import android.widget.LinearLayout; |
||||
|
import android.widget.TextView; |
||||
|
|
||||
|
import androidx.annotation.Nullable; |
||||
|
|
||||
|
public class RecordingFloatingService extends Service { |
||||
|
static final String ACTION_SHOW = "com.example.native_plugin.action.SHOW_RECORDING_FLOATING_BAR"; |
||||
|
static final String ACTION_UPDATE = "com.example.native_plugin.action.UPDATE_RECORDING_FLOATING_BAR"; |
||||
|
static final String ACTION_HIDE = "com.example.native_plugin.action.HIDE_RECORDING_FLOATING_BAR"; |
||||
|
|
||||
|
static final String EXTRA_DURATION = "duration"; |
||||
|
static final String EXTRA_RECORDING_TYPE = "recordingType"; |
||||
|
static final String EXTRA_IS_PAUSED = "isPaused"; |
||||
|
|
||||
|
private WindowManager windowManager; |
||||
|
private View floatingView; |
||||
|
private WindowManager.LayoutParams layoutParams; |
||||
|
private TextView durationTextView; |
||||
|
private TextView typeTextView; |
||||
|
private TextView playPauseTextView; |
||||
|
private boolean isPaused = false; |
||||
|
|
||||
|
@Override |
||||
|
public void onCreate() { |
||||
|
super.onCreate(); |
||||
|
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int onStartCommand(Intent intent, int flags, int startId) { |
||||
|
if (intent == null) { |
||||
|
return START_NOT_STICKY; |
||||
|
} |
||||
|
|
||||
|
String action = intent.getAction(); |
||||
|
if (ACTION_HIDE.equals(action)) { |
||||
|
removeFloatingView(); |
||||
|
stopSelf(); |
||||
|
return START_NOT_STICKY; |
||||
|
} |
||||
|
|
||||
|
if (ACTION_SHOW.equals(action) || ACTION_UPDATE.equals(action)) { |
||||
|
String duration = intent.getStringExtra(EXTRA_DURATION); |
||||
|
String recordingType = intent.getStringExtra(EXTRA_RECORDING_TYPE); |
||||
|
boolean paused = intent.getBooleanExtra(EXTRA_IS_PAUSED, false); |
||||
|
showOrUpdate(duration, recordingType, paused); |
||||
|
} |
||||
|
|
||||
|
return START_STICKY; |
||||
|
} |
||||
|
|
||||
|
private void showOrUpdate(String duration, String recordingType, boolean paused) { |
||||
|
if (floatingView == null) { |
||||
|
createFloatingView(); |
||||
|
addFloatingView(); |
||||
|
} |
||||
|
updateContent(duration, recordingType, paused); |
||||
|
} |
||||
|
|
||||
|
private void updateContent(String duration, String recordingType, boolean paused) { |
||||
|
isPaused = paused; |
||||
|
updatePlayPauseIcon(); |
||||
|
if (durationTextView != null) { |
||||
|
durationTextView.setText(duration == null ? "" : duration); |
||||
|
} |
||||
|
if (typeTextView != null) { |
||||
|
String type = recordingType == null ? "" : recordingType.trim(); |
||||
|
if (type.isEmpty()) { |
||||
|
typeTextView.setVisibility(View.GONE); |
||||
|
} else { |
||||
|
typeTextView.setVisibility(View.VISIBLE); |
||||
|
typeTextView.setText(type); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void updatePlayPauseIcon() { |
||||
|
if (playPauseTextView == null) return; |
||||
|
playPauseTextView.setText(isPaused ? "▶" : "⏸"); |
||||
|
} |
||||
|
|
||||
|
private void createFloatingView() { |
||||
|
float density = getResources().getDisplayMetrics().density; |
||||
|
int touchSlop = ViewConfiguration.get(this).getScaledTouchSlop(); |
||||
|
|
||||
|
LinearLayout container = new LinearLayout(this); |
||||
|
container.setOrientation(LinearLayout.HORIZONTAL); |
||||
|
container.setGravity(Gravity.CENTER_VERTICAL); |
||||
|
int paddingH = (int) (10 * density); |
||||
|
int paddingV = (int) (6 * density); |
||||
|
container.setPadding(paddingH, paddingV, paddingH, paddingV); |
||||
|
|
||||
|
GradientDrawable bg = new GradientDrawable(); |
||||
|
bg.setColor(Color.parseColor("#1A1A1A")); |
||||
|
bg.setCornerRadius(999 * density); |
||||
|
container.setBackground(bg); |
||||
|
|
||||
|
playPauseTextView = new TextView(this); |
||||
|
int playSize = (int) (26 * density); |
||||
|
LinearLayout.LayoutParams playLp = new LinearLayout.LayoutParams(playSize, playSize); |
||||
|
playLp.rightMargin = (int) (10 * density); |
||||
|
playPauseTextView.setLayoutParams(playLp); |
||||
|
playPauseTextView.setTextColor(Color.WHITE); |
||||
|
playPauseTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); |
||||
|
playPauseTextView.setGravity(Gravity.CENTER); |
||||
|
GradientDrawable playBg = new GradientDrawable(); |
||||
|
playBg.setColor(Color.parseColor("#FF3B30")); |
||||
|
playBg.setCornerRadius(999 * density); |
||||
|
playPauseTextView.setBackground(playBg); |
||||
|
playPauseTextView.setOnClickListener(v -> { |
||||
|
isPaused = !isPaused; |
||||
|
updatePlayPauseIcon(); |
||||
|
NativePlugin.emitRecordingFloatingAction("togglePause"); |
||||
|
}); |
||||
|
updatePlayPauseIcon(); |
||||
|
container.addView(playPauseTextView); |
||||
|
|
||||
|
LinearLayout middleArea = new LinearLayout(this); |
||||
|
middleArea.setOrientation(LinearLayout.HORIZONTAL); |
||||
|
middleArea.setGravity(Gravity.CENTER_VERTICAL); |
||||
|
LinearLayout.LayoutParams middleLp = new LinearLayout.LayoutParams( |
||||
|
LinearLayout.LayoutParams.WRAP_CONTENT, |
||||
|
LinearLayout.LayoutParams.WRAP_CONTENT |
||||
|
); |
||||
|
middleLp.rightMargin = (int) (10 * density); |
||||
|
middleArea.setLayoutParams(middleLp); |
||||
|
middleArea.setClickable(true); |
||||
|
middleArea.setFocusable(true); |
||||
|
|
||||
|
durationTextView = new TextView(this); |
||||
|
durationTextView.setTextColor(Color.WHITE); |
||||
|
durationTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); |
||||
|
LinearLayout.LayoutParams durationLp = new LinearLayout.LayoutParams( |
||||
|
LinearLayout.LayoutParams.WRAP_CONTENT, |
||||
|
LinearLayout.LayoutParams.WRAP_CONTENT |
||||
|
); |
||||
|
durationLp.rightMargin = (int) (8 * density); |
||||
|
durationTextView.setLayoutParams(durationLp); |
||||
|
durationTextView.setText("00:00"); |
||||
|
middleArea.addView(durationTextView); |
||||
|
|
||||
|
typeTextView = new TextView(this); |
||||
|
typeTextView.setTextColor(Color.parseColor("#E6E6E6")); |
||||
|
typeTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); |
||||
|
LinearLayout.LayoutParams typeLp = new LinearLayout.LayoutParams( |
||||
|
LinearLayout.LayoutParams.WRAP_CONTENT, |
||||
|
LinearLayout.LayoutParams.WRAP_CONTENT |
||||
|
); |
||||
|
typeTextView.setLayoutParams(typeLp); |
||||
|
typeTextView.setVisibility(View.GONE); |
||||
|
middleArea.addView(typeTextView); |
||||
|
|
||||
|
middleArea.setOnClickListener(v -> { |
||||
|
bringAppToFront(); |
||||
|
NativePlugin.emitRecordingFloatingAction("openMeetingRecord"); |
||||
|
}); |
||||
|
|
||||
|
middleArea.setOnTouchListener(new View.OnTouchListener() { |
||||
|
private int initialY; |
||||
|
private float initialTouchY; |
||||
|
private boolean dragging; |
||||
|
|
||||
|
@Override |
||||
|
public boolean onTouch(View v, MotionEvent event) { |
||||
|
if (layoutParams == null || windowManager == null) return false; |
||||
|
switch (event.getAction()) { |
||||
|
case MotionEvent.ACTION_DOWN: |
||||
|
initialY = layoutParams.y; |
||||
|
initialTouchY = event.getRawY(); |
||||
|
dragging = false; |
||||
|
return true; |
||||
|
case MotionEvent.ACTION_MOVE: |
||||
|
int dy = (int) (event.getRawY() - initialTouchY); |
||||
|
if (!dragging && Math.abs(dy) > touchSlop) { |
||||
|
dragging = true; |
||||
|
} |
||||
|
if (!dragging) return true; |
||||
|
|
||||
|
int newY = initialY + dy; |
||||
|
int screenHeight = getResources().getDisplayMetrics().heightPixels; |
||||
|
int viewHeight = floatingView == null ? 0 : floatingView.getHeight(); |
||||
|
int maxY = Math.max(0, screenHeight - viewHeight - (int) (24 * density)); |
||||
|
if (newY < 0) newY = 0; |
||||
|
if (newY > maxY) newY = maxY; |
||||
|
layoutParams.y = newY; |
||||
|
windowManager.updateViewLayout(floatingView, layoutParams); |
||||
|
return true; |
||||
|
case MotionEvent.ACTION_UP: |
||||
|
case MotionEvent.ACTION_CANCEL: |
||||
|
if (!dragging) { |
||||
|
v.performClick(); |
||||
|
} |
||||
|
return true; |
||||
|
default: |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
container.addView(middleArea); |
||||
|
|
||||
|
TextView close = new TextView(this); |
||||
|
int closeSize = (int) (22 * density); |
||||
|
LinearLayout.LayoutParams closeLp = new LinearLayout.LayoutParams(closeSize, closeSize); |
||||
|
close.setLayoutParams(closeLp); |
||||
|
close.setText("×"); |
||||
|
close.setTextColor(Color.WHITE); |
||||
|
close.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); |
||||
|
close.setGravity(Gravity.CENTER); |
||||
|
close.setOnClickListener(v -> { |
||||
|
removeFloatingView(); |
||||
|
stopSelf(); |
||||
|
NativePlugin.emitRecordingFloatingAction("closeFloatingBar"); |
||||
|
}); |
||||
|
container.addView(close); |
||||
|
|
||||
|
floatingView = container; |
||||
|
} |
||||
|
|
||||
|
private void addFloatingView() { |
||||
|
if (windowManager == null || floatingView == null) return; |
||||
|
layoutParams = new WindowManager.LayoutParams( |
||||
|
WindowManager.LayoutParams.WRAP_CONTENT, |
||||
|
WindowManager.LayoutParams.WRAP_CONTENT, |
||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O |
||||
|
? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY |
||||
|
: WindowManager.LayoutParams.TYPE_PHONE, |
||||
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
||||
|
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
||||
|
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, |
||||
|
PixelFormat.TRANSLUCENT |
||||
|
); |
||||
|
|
||||
|
layoutParams.gravity = Gravity.TOP | Gravity.END; |
||||
|
float density = getResources().getDisplayMetrics().density; |
||||
|
layoutParams.x = (int) (6 * density); |
||||
|
layoutParams.y = (int) (48 * density); |
||||
|
|
||||
|
windowManager.addView(floatingView, layoutParams); |
||||
|
} |
||||
|
|
||||
|
private void removeFloatingView() { |
||||
|
if (windowManager != null && floatingView != null) { |
||||
|
try { |
||||
|
windowManager.removeView(floatingView); |
||||
|
} catch (Exception ignored) { |
||||
|
} |
||||
|
floatingView = null; |
||||
|
layoutParams = null; |
||||
|
durationTextView = null; |
||||
|
typeTextView = null; |
||||
|
playPauseTextView = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void bringAppToFront() { |
||||
|
try { |
||||
|
Intent launchIntent = getPackageManager().getLaunchIntentForPackage(getPackageName()); |
||||
|
if (launchIntent == null) return; |
||||
|
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); |
||||
|
startActivity(launchIntent); |
||||
|
} catch (Exception ignored) { |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Nullable |
||||
|
@Override |
||||
|
public IBinder onBind(Intent intent) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onDestroy() { |
||||
|
removeFloatingView(); |
||||
|
super.onDestroy(); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue