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" |
|||
package="com.example.native_plugin"> |
|||
|
|||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> |
|||
|
|||
<application> |
|||
<service |
|||
android:name=".RecordingFloatingService" |
|||
android:exported="false" /> |
|||
</application> |
|||
</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