修复全屏播放和退出登录功能:1.添加退出登录按钮 2.修复全屏播放黑屏问题 3.添加退出全屏按钮 4.修复退出全屏后布局恢复

This commit is contained in:
xiao12feng8 2026-01-06 18:42:23 +08:00
parent 202c8452c5
commit b2f848c918
17 changed files with 903 additions and 466 deletions

View File

@ -11,7 +11,9 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -69,13 +71,13 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
private boolean streamerVerified = false; private boolean streamerVerified = false;
private boolean cameraInitialized = false; private boolean cameraInitialized = false;
// 推流参数 - 竖屏直播使用9:16分辨率 // 推流参数 - 竖屏直播
// 手机竖屏直播直接使用竖屏分辨率不需要交换宽高 // 使用 9:16 或接近手机屏幕比例的分辨率
private static final int VIDEO_WIDTH = 720; // 竖屏宽度 private static final int VIDEO_WIDTH = 720; // 竖屏宽度
private static final int VIDEO_HEIGHT = 1280; // 竖屏高度 (9:16) private static final int VIDEO_HEIGHT = 1280; // 竖屏高度 (9:16)
private static final int VIDEO_FPS = 25; // 标准帧率 private static final int VIDEO_FPS = 30; // 提高帧率
private static final int VIDEO_BITRATE = 1500 * 1024; // 1.5Mbps高清 private static final int VIDEO_BITRATE = 2000 * 1024; // 2Mbps高清
private static final int AUDIO_BITRATE = 64 * 1024; private static final int AUDIO_BITRATE = 128 * 1024; // 128kbps 音频
private static final int AUDIO_SAMPLE_RATE = 44100; private static final int AUDIO_SAMPLE_RATE = 44100;
// 直播计时 // 直播计时
@ -83,6 +85,10 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
private Handler mainHandler = new Handler(Looper.getMainLooper()); private Handler mainHandler = new Handler(Looper.getMainLooper());
private long startTime = 0; private long startTime = 0;
private Runnable timerRunnable; private Runnable timerRunnable;
// 预览尺寸用于计算 centerCrop
private int previewWidth = 0;
private int previewHeight = 0;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -316,8 +322,12 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
} }
private void initCameraInternal() { private void initCameraInternal() {
// 获取屏幕尺寸
int screenWidth = getResources().getDisplayMetrics().widthPixels;
int screenHeight = getResources().getDisplayMetrics().heightPixels;
Log.d(TAG, "屏幕尺寸: " + screenWidth + "x" + screenHeight);
// 手机直播使用竖屏分辨率 // 手机直播使用竖屏分辨率
// RootEncoder 会自动处理摄像头旋转
int videoWidth = VIDEO_WIDTH; // 720 int videoWidth = VIDEO_WIDTH; // 720
int videoHeight = VIDEO_HEIGHT; // 1280 int videoHeight = VIDEO_HEIGHT; // 1280
Log.d(TAG, "目标视频分辨率: " + videoWidth + "x" + videoHeight + " (竖屏9:16)"); Log.d(TAG, "目标视频分辨率: " + videoWidth + "x" + videoHeight + " (竖屏9:16)");
@ -355,11 +365,46 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
rtmpCamera2 = null; rtmpCamera2 = null;
} else { } else {
// 编码器准备好后开始预览 // 编码器准备好后开始预览
// 使用 CameraHelper.Facing 来指定摄像头
CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK; CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK;
rtmpCamera2.startPreview(facing, videoWidth, videoHeight);
// 使用摄像头原生支持的分辨率
// 先尝试自动分辨率让库自己选择最佳分辨率
boolean previewStarted = false;
try {
// 直接使用自动分辨率避免分辨率不支持的问题
rtmpCamera2.startPreview(facing);
Log.d(TAG, "Camera2 预览启动成功(自动分辨率)");
previewStarted = true;
// 使用默认的 640x480 计算 centerCrop
adjustPreviewSize(640, 480);
} catch (Exception e) {
Log.w(TAG, "自动分辨率预览失败: " + e.getMessage());
}
if (!previewStarted) {
// 自动分辨率失败尝试常见分辨率
int[][] resolutions = {
{640, 480}, // VGA 4:3
{1280, 720}, // HD 16:9
{1920, 1080}, // Full HD 16:9
};
for (int[] res : resolutions) {
try {
rtmpCamera2.startPreview(facing, res[0], res[1]);
Log.d(TAG, "Camera2 预览启动成功: " + res[0] + "x" + res[1]);
previewStarted = true;
adjustPreviewSize(res[0], res[1]);
break;
} catch (Exception e) {
Log.w(TAG, "预览分辨率 " + res[0] + "x" + res[1] + " 不支持: " + e.getMessage());
}
}
}
useCamera2 = true; useCamera2 = true;
Log.d(TAG, "Camera2 + OpenGlView 初始化完成,预览已开始"); Log.d(TAG, "Camera2 + OpenGlView 初始化完成");
return; return;
} }
} catch (Exception e) { } catch (Exception e) {
@ -401,10 +446,42 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
return; return;
} }
// 使用摄像头原生支持的分辨率
CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK; CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK;
rtmpCamera1.startPreview(facing);
// 先尝试自动分辨率
boolean previewStarted = false;
try {
rtmpCamera1.startPreview(facing);
Log.d(TAG, "Camera1 预览启动成功(自动分辨率)");
previewStarted = true;
adjustPreviewSize(640, 480);
} catch (Exception e) {
Log.w(TAG, "自动分辨率预览失败: " + e.getMessage());
}
if (!previewStarted) {
int[][] resolutions = {
{640, 480},
{1280, 720},
};
for (int[] res : resolutions) {
try {
rtmpCamera1.startPreview(facing, res[0], res[1]);
Log.d(TAG, "Camera1 预览启动成功: " + res[0] + "x" + res[1]);
previewStarted = true;
adjustPreviewSize(res[0], res[1]);
break;
} catch (Exception e) {
Log.w(TAG, "预览分辨率 " + res[0] + "x" + res[1] + " 不支持: " + e.getMessage());
}
}
}
useCamera2 = false; useCamera2 = false;
Log.d(TAG, "Camera1 + OpenGlView 初始化完成,预览已开始"); Log.d(TAG, "Camera1 + OpenGlView 初始化完成");
return; return;
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Camera1 初始化也失败: " + e.getMessage(), e); Log.e(TAG, "Camera1 初始化也失败: " + e.getMessage(), e);
@ -424,6 +501,52 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
private boolean hasCamera() { private boolean hasCamera() {
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); return getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
} }
/**
* 调整 OpenGlView 尺寸实现 centerCrop 效果
* 让预览画面填满屏幕裁剪多余部分
*/
private void adjustPreviewSize(int cameraWidth, int cameraHeight) {
// 获取容器尺寸
int containerWidth = binding.previewContainer.getWidth();
int containerHeight = binding.previewContainer.getHeight();
if (containerWidth == 0 || containerHeight == 0) {
Log.w(TAG, "容器尺寸为0延迟调整");
mainHandler.postDelayed(() -> adjustPreviewSize(cameraWidth, cameraHeight), 100);
return;
}
// 摄像头输出是横向的竖屏显示时需要交换宽高
int previewW = cameraHeight; // 旋转后的宽度
int previewH = cameraWidth; // 旋转后的高度
Log.d(TAG, "容器尺寸: " + containerWidth + "x" + containerHeight);
Log.d(TAG, "预览尺寸(旋转后): " + previewW + "x" + previewH);
// 计算 centerCrop 需要的缩放比例
float containerRatio = (float) containerWidth / containerHeight;
float previewRatio = (float) previewW / previewH;
int newWidth, newHeight;
if (previewRatio > containerRatio) {
// 预览更宽以高度为基准宽度会超出
newHeight = containerHeight;
newWidth = (int) (containerHeight * previewRatio);
} else {
// 预览更高以宽度为基准高度会超出
newWidth = containerWidth;
newHeight = (int) (containerWidth / previewRatio);
}
Log.d(TAG, "调整后的 OpenGlView 尺寸: " + newWidth + "x" + newHeight);
// 更新 OpenGlView 尺寸
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(newWidth, newHeight);
params.gravity = android.view.Gravity.CENTER;
binding.openGlView.setLayoutParams(params);
}
private void switchCamera() { private void switchCamera() {
isFrontCamera = !isFrontCamera; isFrontCamera = !isFrontCamera;

View File

@ -278,6 +278,7 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
// 返回按钮 // 返回按钮
binding.backButton.setOnClickListener(v -> finish()); binding.backButton.setOnClickListener(v -> finish());
// 退出全屏按钮点击事件
binding.exitFullscreenButton.setOnClickListener(v -> { binding.exitFullscreenButton.setOnClickListener(v -> {
if (isFullscreen) { if (isFullscreen) {
toggleFullscreen(); toggleFullscreen();
@ -285,16 +286,6 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
finish(); finish();
} }
}); });
View.OnClickListener fullscreenOverlayToggle = v -> {
if (!isFullscreen) return;
int vis = binding.exitFullscreenButton.getVisibility();
binding.exitFullscreenButton.setVisibility(vis == View.VISIBLE ? View.GONE : View.VISIBLE);
};
// PlayerView 往往会自己消费触摸事件导致 container 的点击回调不触发所以这里两处都绑定
binding.playerContainer.setOnClickListener(fullscreenOverlayToggle);
binding.playerView.setOnClickListener(fullscreenOverlayToggle);
// 全屏按钮 // 全屏按钮
binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen()); binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen());
@ -325,14 +316,26 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
* 手机视频全屏显示100%高度UI元素覆盖在视频上 * 手机视频全屏显示100%高度UI元素覆盖在视频上
*/ */
private void adjustVideoPlayerHeight() { private void adjustVideoPlayerHeight() {
// 初始化时不做任何调整等待视频流加载后根据视频宽高比调整 // 初始化视频区域高度为屏幕的45%
android.util.Log.d("RoomDetail", "等待视频流加载后根据视频宽高比调整显示方式..."); android.util.DisplayMetrics displayMetrics = new android.util.DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int screenHeight = displayMetrics.heightPixels;
int videoHeight = (int) (screenHeight * 0.45);
android.widget.FrameLayout.LayoutParams playerParams =
(android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.height = videoHeight;
playerParams.gravity = android.view.Gravity.CENTER;
binding.playerContainer.setLayoutParams(playerParams);
android.util.Log.d("RoomDetail", "初始化视频区域高度: " + videoHeight + " (屏幕高度的45%)");
} }
/** /**
* 根据视频流的宽高比调整显示方式 * 根据视频流的宽高比调整显示方式
* 手机推流竖屏视频高度>宽度视频全屏显示 * 手机推流竖屏视频高度>宽度视频全屏显示
* 电脑推流横屏视频宽度>高度视频显示50%高度 * 电脑推流横屏视频宽度>高度视频在中间显示保持比例
*/ */
private void adjustVideoDisplayByStreamType(int videoWidth, int videoHeight) { private void adjustVideoDisplayByStreamType(int videoWidth, int videoHeight) {
try { try {
@ -343,48 +346,54 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
int screenWidth = displayMetrics.widthPixels; int screenWidth = displayMetrics.widthPixels;
// 判断视频是竖屏还是横屏 // 判断视频是竖屏还是横屏
// 手机推流视频是竖屏的高度 > 宽度
// 电脑推流视频是横屏的宽度 > 高度
boolean isMobileStream = videoHeight > videoWidth; boolean isMobileStream = videoHeight > videoWidth;
android.util.Log.d("RoomDetail", "视频尺寸: " + videoWidth + "x" + videoHeight + android.util.Log.d("RoomDetail", "视频尺寸: " + videoWidth + "x" + videoHeight +
", 屏幕尺寸: " + screenWidth + "x" + screenHeight + ", 屏幕尺寸: " + screenWidth + "x" + screenHeight +
", 推流类型: " + (isMobileStream ? "手机(竖屏)" : "电脑(横屏)")); ", 推流类型: " + (isMobileStream ? "手机(竖屏)" : "电脑(横屏)"));
// 无论什么类型的推流都让视频全屏显示 android.widget.FrameLayout.LayoutParams playerParams =
// 手机推流的竖屏视频会自动填满屏幕 (android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
// 电脑推流的横屏视频会在上下留黑边保持比例
android.util.Log.d("RoomDetail", "视频全屏显示模式"); if (isMobileStream) {
// 手机推流竖屏视频全屏显示
android.util.Log.d("RoomDetail", "手机推流 - 全屏显示模式");
playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.gravity = android.view.Gravity.CENTER;
} else {
// 电脑推流横屏视频在中间显示保持16:9比例
android.util.Log.d("RoomDetail", "电脑推流 - 居中显示模式");
// 计算16:9比例的高度
float aspectRatio = (float) videoWidth / videoHeight;
int targetHeight = (int) (screenWidth / aspectRatio);
// 限制最大高度为屏幕的60%
int maxHeight = (int) (screenHeight * 0.6);
if (targetHeight > maxHeight) {
targetHeight = maxHeight;
}
playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.height = targetHeight;
playerParams.gravity = android.view.Gravity.CENTER;
android.util.Log.d("RoomDetail", "视频区域高度: " + targetHeight + " (屏幕高度的" +
(targetHeight * 100 / screenHeight) + "%)");
}
// 设置 playerContainer 全屏
android.view.ViewGroup.LayoutParams playerParams = binding.playerContainer.getLayoutParams();
playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
binding.playerContainer.setLayoutParams(playerParams); binding.playerContainer.setLayoutParams(playerParams);
// 设置 SurfaceView 全屏填充 // 设置 SurfaceView
if (binding.flvSurfaceView != null && binding.flvSurfaceView.getVisibility() == View.VISIBLE) { if (binding.flvSurfaceView != null && binding.flvSurfaceView.getVisibility() == View.VISIBLE) {
android.view.ViewGroup.LayoutParams surfaceParams = binding.flvSurfaceView.getLayoutParams(); android.view.ViewGroup.LayoutParams surfaceParams = binding.flvSurfaceView.getLayoutParams();
surfaceParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT; surfaceParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
surfaceParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT; surfaceParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
binding.flvSurfaceView.setLayoutParams(surfaceParams); binding.flvSurfaceView.setLayoutParams(surfaceParams);
// 关键设置 SurfaceView 的固定尺寸以保持视频比例
// 对于竖屏视频让它填满屏幕宽度高度按比例计算
if (isMobileStream) {
// 竖屏视频宽度填满屏幕高度按比例
float aspectRatio = (float) videoHeight / videoWidth;
int targetHeight = (int) (screenWidth * aspectRatio);
if (targetHeight > screenHeight) {
// 如果计算出的高度超过屏幕则以屏幕高度为准
targetHeight = screenHeight;
}
binding.flvSurfaceView.getHolder().setFixedSize(screenWidth, targetHeight);
android.util.Log.d("RoomDetail", "SurfaceView 固定尺寸: " + screenWidth + "x" + targetHeight);
}
} }
// 设置 PlayerView 全屏填充 // 设置 PlayerView
if (binding.playerView != null) { if (binding.playerView != null) {
android.view.ViewGroup.LayoutParams pvParams = binding.playerView.getLayoutParams(); android.view.ViewGroup.LayoutParams pvParams = binding.playerView.getLayoutParams();
pvParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT; pvParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
@ -624,28 +633,42 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
// 停止之前的重连任务 // 停止之前的重连任务
stopChatReconnect(); stopChatReconnect();
// 获取用户token
String token = AuthStore.getToken(this);
// 构建WebSocket URL添加token参数
String wsUrl = getWsChatBaseUrl() + roomId; String wsUrl = getWsChatBaseUrl() + roomId;
if (!TextUtils.isEmpty(token)) {
wsUrl += "?token=" + token;
}
android.util.Log.d("ChatWebSocket", "准备连接WebSocket: " + wsUrl); android.util.Log.d("ChatWebSocket", "准备连接WebSocket: " + wsUrl);
chatWsClient = new OkHttpClient.Builder() try {
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping chatWsClient = new OkHttpClient.Builder()
.build(); .pingInterval(30, java.util.concurrent.TimeUnit.SECONDS)
Request request = new Request.Builder() .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.url(wsUrl) .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build(); .build();
Request request = new Request.Builder()
chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() { .url(wsUrl)
@Override .build();
public void onOpen(WebSocket webSocket, okhttp3.Response response) {
android.util.Log.d("ChatWebSocket", "弹幕连接成功: roomId=" + roomId);
isChatWebSocketConnected = true;
chatReconnectAttempts = 0;
// 启动心跳检测
startChatHeartbeat();
}
@Override chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
public void onMessage(WebSocket webSocket, String text) { @Override
public void onOpen(WebSocket webSocket, okhttp3.Response response) {
android.util.Log.d("ChatWebSocket", "弹幕连接成功: roomId=" + roomId);
isChatWebSocketConnected = true;
chatReconnectAttempts = 0;
// 启动心跳检测
startChatHeartbeat();
handler.post(() -> {
addChatMessage(new ChatMessage("已连接到弹幕服务", true));
});
}
@Override
public void onMessage(WebSocket webSocket, String text) {
// 收到消息解析并显示 // 收到消息解析并显示
android.util.Log.d("ChatWebSocket", "收到消息: " + text); android.util.Log.d("ChatWebSocket", "收到消息: " + text);
try { try {
@ -709,9 +732,19 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
@Override @Override
public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) { public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) {
android.util.Log.e("ChatWebSocket", "连接失败: " + t.getMessage()); String errorMsg = t != null ? t.getMessage() : "未知错误";
int responseCode = response != null ? response.code() : -1;
android.util.Log.e("ChatWebSocket", "连接失败: " + errorMsg + ", responseCode=" + responseCode);
isChatWebSocketConnected = false; isChatWebSocketConnected = false;
stopChatHeartbeat(); stopChatHeartbeat();
// 在UI上显示连接状态仅首次失败时显示
if (chatReconnectAttempts == 0) {
handler.post(() -> {
addChatMessage(new ChatMessage("弹幕服务连接中...", true));
});
}
// 尝试重连 // 尝试重连
scheduleChatReconnect(); scheduleChatReconnect();
} }
@ -727,6 +760,11 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
} }
} }
}); });
} catch (Exception e) {
android.util.Log.e("ChatWebSocket", "创建WebSocket失败: " + e.getMessage());
isChatWebSocketConnected = false;
scheduleChatReconnect();
}
} }
/** /**
@ -949,10 +987,17 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
} }
private void sendChatViaWebSocket(String content) { private void sendChatViaWebSocket(String content) {
// 先本地显示消息乐观更新
String nickname = AuthStore.getNickname(this);
if (TextUtils.isEmpty(nickname)) {
nickname = "";
}
addChatMessage(new ChatMessage(nickname, content));
if (chatWebSocket == null || !isChatWebSocketConnected) { if (chatWebSocket == null || !isChatWebSocketConnected) {
// 如果 WebSocket 未连接先本地显示 // WebSocket 未连接尝试重连
addChatMessage(new ChatMessage("", content)); android.util.Log.w("ChatWebSocket", "WebSocket未连接尝试重连...");
Toast.makeText(this, "弹幕连接断开,消息仅本地显示", Toast.LENGTH_SHORT).show(); connectChatWebSocket();
return; return;
} }
@ -960,14 +1005,15 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
json.put("type", "chat"); json.put("type", "chat");
json.put("content", content); json.put("content", content);
json.put("nickname", AuthStore.getNickname(this)); json.put("nickname", nickname);
json.put("userId", AuthStore.getUserId(this)); json.put("userId", AuthStore.getUserId(this));
chatWebSocket.send(json.toString()); boolean sent = chatWebSocket.send(json.toString());
if (!sent) {
android.util.Log.w("ChatWebSocket", "消息发送失败WebSocket可能已断开");
}
} catch (JSONException e) { } catch (JSONException e) {
android.util.Log.e("ChatWebSocket", "发送消息失败: " + e.getMessage()); android.util.Log.e("ChatWebSocket", "发送消息失败: " + e.getMessage());
// 失败时本地显示
addChatMessage(new ChatMessage("", content));
} }
} }
@ -1020,25 +1066,63 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
// 退出全屏 // 退出全屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
// ActionBar可能为null需要检查 // 恢复系统UI
androidx.appcompat.app.ActionBar actionBar = getSupportActionBar(); getWindow().getDecorView().setSystemUiVisibility(
if (actionBar != null) { View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
actionBar.show();
}
isFullscreen = false; isFullscreen = false;
if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE); if (binding != null) {
binding.exitFullscreenButton.setVisibility(View.GONE);
binding.fullscreenButton.setVisibility(View.VISIBLE);
// 恢复视频区域大小 - 使用屏幕高度的50%
android.util.DisplayMetrics displayMetrics = new android.util.DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int screenHeight = displayMetrics.heightPixels;
int videoHeight = (int) (screenHeight * 0.45); // 45%屏幕高度
android.widget.FrameLayout.LayoutParams playerParams =
(android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.height = videoHeight;
playerParams.gravity = android.view.Gravity.CENTER;
playerParams.setMargins(0, 0, 0, 0);
binding.playerContainer.setLayoutParams(playerParams);
// 显示所有UI元素
binding.uiOverlayContainer.setVisibility(View.VISIBLE);
}
} else { } else {
// 进入全屏 // 进入全屏横屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN); WindowManager.LayoutParams.FLAG_FULLSCREEN);
// ActionBar可能为null需要检查 // 隐藏系统UI状态栏和导航栏
androidx.appcompat.app.ActionBar actionBar = getSupportActionBar(); getWindow().getDecorView().setSystemUiVisibility(
if (actionBar != null) { View.SYSTEM_UI_FLAG_FULLSCREEN
actionBar.hide(); | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
} | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
isFullscreen = true; isFullscreen = true;
if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE); if (binding != null) {
binding.exitFullscreenButton.setVisibility(View.VISIBLE);
binding.fullscreenButton.setVisibility(View.GONE);
// 全屏时视频区域填满屏幕
android.widget.FrameLayout.LayoutParams playerParams =
(android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.gravity = android.view.Gravity.CENTER;
playerParams.setMargins(0, 0, 0, 0);
binding.playerContainer.setLayoutParams(playerParams);
// 隐藏UI覆盖层除了退出按钮
binding.uiOverlayContainer.setVisibility(View.GONE);
}
} }
} }
@ -1065,18 +1149,59 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 横屏时隐藏其他UI元素只显示播放器 // 横屏时隐藏其他UI元素只显示播放器和退出按钮
binding.topBar.setVisibility(View.GONE); isFullscreen = true;
binding.roomInfoLayout.setVisibility(View.GONE);
binding.chatInputLayout.setVisibility(View.GONE); // 隐藏系统UI
binding.chatRecyclerView.setVisibility(View.GONE); getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
// 隐藏UI覆盖层
binding.uiOverlayContainer.setVisibility(View.GONE);
// 全屏时视频区域填满屏幕
android.widget.FrameLayout.LayoutParams playerParams =
(android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.gravity = android.view.Gravity.CENTER;
playerParams.setMargins(0, 0, 0, 0);
binding.playerContainer.setLayoutParams(playerParams);
// 显示退出全屏按钮在playerContainer内
binding.exitFullscreenButton.setVisibility(View.VISIBLE); binding.exitFullscreenButton.setVisibility(View.VISIBLE);
binding.fullscreenButton.setVisibility(View.GONE);
} else { } else {
// 竖屏时显示所有UI元素 // 竖屏时显示所有UI元素
binding.topBar.setVisibility(View.VISIBLE); isFullscreen = false;
binding.roomInfoLayout.setVisibility(View.VISIBLE);
binding.chatInputLayout.setVisibility(View.VISIBLE); // 恢复系统UI
binding.chatRecyclerView.setVisibility(View.VISIBLE); getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
// 显示UI覆盖层
binding.uiOverlayContainer.setVisibility(View.VISIBLE);
// 竖屏时恢复视频区域大小 - 使用屏幕高度的45%
android.util.DisplayMetrics displayMetrics = new android.util.DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int screenHeight = displayMetrics.heightPixels;
int videoHeight = (int) (screenHeight * 0.45);
android.widget.FrameLayout.LayoutParams playerParams =
(android.widget.FrameLayout.LayoutParams) binding.playerContainer.getLayoutParams();
playerParams.width = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
playerParams.height = videoHeight;
playerParams.gravity = android.view.Gravity.CENTER;
playerParams.setMargins(0, 0, 0, 0);
binding.playerContainer.setLayoutParams(playerParams);
binding.fullscreenButton.setVisibility(View.VISIBLE);
binding.exitFullscreenButton.setVisibility(View.GONE); binding.exitFullscreenButton.setVisibility(View.GONE);
} }
} }

View File

@ -133,6 +133,11 @@ public class SettingsPageActivity extends AppCompatActivity {
startActivity(intent); startActivity(intent);
return; return;
} }
if ("退出登录".equals(t)) {
showLogoutDialog();
return;
}
// 处理其他页面的点击事件 // 处理其他页面的点击事件
if (PAGE_SERVER.equals(page)) { if (PAGE_SERVER.equals(page)) {
@ -385,6 +390,12 @@ public class SettingsPageActivity extends AppCompatActivity {
list.add(MoreItem.row("申请成为主播", "认证后可开播", R.drawable.ic_live_24)); list.add(MoreItem.row("申请成为主播", "认证后可开播", R.drawable.ic_live_24));
} }
// 退出登录仅在已登录时显示
if (com.example.livestreaming.net.AuthStore.getToken(this) != null) {
list.add(MoreItem.section("账号"));
list.add(MoreItem.row("退出登录", "退出当前账号", R.drawable.ic_person_24));
}
return list; return list;
} }
@ -851,6 +862,25 @@ public class SettingsPageActivity extends AppCompatActivity {
.show(); .show();
} }
private void showLogoutDialog() {
new AlertDialog.Builder(this)
.setTitle("退出登录")
.setMessage("确定要退出当前账号吗?")
.setPositiveButton("确定", (d, w) -> {
// 清除所有登录数据
com.example.livestreaming.net.AuthStore.clearAll(this);
Toast.makeText(this, "已退出登录", Toast.LENGTH_SHORT).show();
// 跳转到登录页面并清除任务栈
Intent intent = new Intent(this, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
})
.setNegativeButton("取消", null)
.show();
}
private int dp(int value) { private int dp(int value) {
return (int) (value * getResources().getDisplayMetrics().density); return (int) (value * getResources().getDisplayMetrics().density);
} }

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#33FFFFFF" />
<corners android:radius="20dp" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 直播间操作按钮圆形背景 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#40000000" />
<size android:width="44dp" android:height="44dp" />
</shape>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <shape xmlns:android="http://schemas.android.com/apk/res/android"
<solid android:color="#E53935" /> android:shape="rectangle">
<corners android:radius="4dp" /> <solid android:color="#FF4444" />
<corners android:radius="12dp" />
</shape> </shape>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- 直播中红色标签 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="#FF4757" /> <solid android:color="#FF4757" />
<corners android:radius="10dp" /> <corners android:radius="4dp" />
</shape> </shape>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 直播间底部渐变遮罩 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:startColor="#90000000"
android:centerColor="#50000000"
android:endColor="#00000000"
android:type="linear" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 直播间聊天输入框背景 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#40FFFFFF" />
<corners android:radius="20dp" />
<stroke
android:width="1dp"
android:color="#30FFFFFF" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 关注按钮背景 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FF4757" />
<corners android:radius="14dp" />
</shape>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 礼物按钮渐变背景 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:startColor="#FF6B9D"
android:endColor="#A855F7"
android:type="linear" />
<corners android:radius="22dp" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#FF4444" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 主播信息胶囊背景 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#60000000" />
<corners android:radius="20dp" />
</shape>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 直播间顶部渐变遮罩 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:startColor="#80000000"
android:centerColor="#40000000"
android:endColor="#00000000"
android:type="linear" />
</shape>

View File

@ -5,16 +5,23 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#000000"> android:background="#000000">
<!-- 使用 OpenGlView 作为摄像头预览,全屏填充显示 --> <!-- 使用 FrameLayout 包裹 OpenGlView实现 centerCrop 效果 -->
<com.pedro.rtplibrary.view.OpenGlView <FrameLayout
android:id="@+id/openGlView" android:id="@+id/previewContainer"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:clipChildren="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent">
app:keepAspectRatio="false" />
<com.pedro.rtplibrary.view.OpenGlView
android:id="@+id/openGlView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
</FrameLayout>
<!-- 顶部工具栏 --> <!-- 顶部工具栏 -->
<LinearLayout <LinearLayout

View File

@ -2,290 +2,374 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:background="#000000">
<ProgressBar <!-- 顶部主播信息区域 -->
android:id="@+id/loading" <LinearLayout
android:layout_width="wrap_content" android:id="@+id/topBar"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:fillViewport="true" android:background="@drawable/bg_gradient_top"
app:layout_constraintBottom_toBottomOf="parent" android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="12dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout <!-- 返回按钮 -->
android:layout_width="match_parent" <ImageButton
android:id="@+id/backButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/bg_circle_semi_transparent"
android:contentDescription="返回"
android:src="@drawable/ic_arrow_back_24"
app:tint="@android:color/white" />
<!-- 主播头像 -->
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/streamerAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="12dp"
android:src="@drawable/ic_person_24" />
<!-- 主播信息 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp"> android:layout_marginStart="8dp"
android:layout_weight="1"
<com.google.android.material.button.MaterialButton android:orientation="vertical">
android:id="@+id/backButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="返回"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/liveBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="6dp"
android:text="未开播"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Room"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/backButton" />
<TextView <TextView
android:id="@+id/streamerText" android:id="@+id/streamerText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="主播: "
android:textColor="#666666"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleText" />
<FrameLayout
android:id="@+id/playerContainer"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="14dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:background="#000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/streamerText">
<androidx.media3.ui.PlayerView
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:resize_mode="fit"
app:show_buffering="when_playing"
app:use_controller="true" />
</FrameLayout>
<TextView
android:id="@+id/offlineHint"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:gravity="center"
android:padding="18dp"
android:text="主播暂未开播"
android:textColor="#666666"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/streamerText" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/playerOrOfflineBarrier"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="bottom" android:ellipsize="end"
app:constraint_referenced_ids="playerContainer,offlineHint" /> android:maxLines="1"
android:text="主播名称"
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="bold" />
<TextView <TextView
android:id="@+id/streamInfoTitle" android:id="@+id/viewerCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 观看"
android:textColor="#CCCCCC"
android:textSize="12sp" />
</LinearLayout>
<!-- 直播状态标签 -->
<TextView
android:id="@+id/liveBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_live_badge"
android:paddingHorizontal="10dp"
android:paddingVertical="4dp"
android:text="直播中"
android:textColor="@android:color/white"
android:textSize="12sp" />
<!-- 关闭按钮 -->
<ImageButton
android:id="@+id/closeButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="12dp"
android:background="@drawable/bg_circle_semi_transparent"
android:contentDescription="关闭"
android:src="@drawable/ic_close_24"
app:tint="@android:color/white" />
</LinearLayout>
<!-- 直播标题 -->
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="2"
android:text="直播标题"
android:textColor="@android:color/white"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topBar" />
<!-- 视频播放区域 - 16:9 比例,点击可全屏 -->
<FrameLayout
android:id="@+id/playerContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="12dp"
android:background="#000000"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleText">
<androidx.media3.ui.PlayerView
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:resize_mode="fit"
app:show_buffering="when_playing"
app:use_controller="false" />
<!-- 全屏按钮 -->
<ImageButton
android:id="@+id/fullscreenButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end|bottom"
android:layout_margin="8dp"
android:background="@drawable/bg_circle_semi_transparent"
android:contentDescription="全屏"
android:src="@drawable/ic_fullscreen_24"
app:tint="@android:color/white" />
<!-- 未开播提示 -->
<TextView
android:id="@+id/offlineHint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="主播暂未开播"
android:textColor="#999999"
android:textSize="16sp"
android:visibility="gone" />
<!-- 加载指示器 -->
<ProgressBar
android:id="@+id/loading"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
<!-- 弹幕/聊天区域 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/chatRecyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:paddingHorizontal="12dp"
app:layout_constraintBottom_toTopOf="@id/bottomBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/playerContainer" />
<!-- 底部互动栏 -->
<LinearLayout
android:id="@+id/bottomBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_gradient_bottom"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<!-- 弹幕输入框 -->
<EditText
android:id="@+id/chatInput"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:background="@drawable/bg_chat_input"
android:hint="说点什么..."
android:inputType="text"
android:maxLines="1"
android:paddingHorizontal="16dp"
android:textColor="@android:color/white"
android:textColorHint="#999999"
android:textSize="14sp" />
<!-- 点赞按钮 -->
<ImageButton
android:id="@+id/likeButton"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_circle_semi_transparent"
android:contentDescription="点赞"
android:src="@drawable/ic_like_24"
app:tint="#FF6B6B" />
<!-- 点赞数 -->
<TextView
android:id="@+id/likeCountText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="0"
android:textColor="@android:color/white"
android:textSize="12sp" />
<!-- 礼物按钮 -->
<ImageButton
android:id="@+id/giftButton"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_circle_semi_transparent"
android:contentDescription="礼物"
android:src="@drawable/ic_gift_24"
app:tint="#FFD700" />
<!-- 发送按钮 -->
<ImageButton
android:id="@+id/sendButton"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_live_send_btn"
android:contentDescription="发送"
android:src="@drawable/ic_send_24"
app:tint="@android:color/white" />
</LinearLayout>
<!-- 推流信息区域(仅房主可见) -->
<LinearLayout
android:id="@+id/streamInfoLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_rounded_semi_transparent"
android:orientation="vertical"
android:padding="12dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/bottomBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📡 推流信息"
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/rtmpValue"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="18dp" android:layout_weight="1"
android:text="推流信息" android:ellipsize="middle"
android:textSize="16sp" android:singleLine="true"
android:textStyle="bold" android:text="rtmp://..."
app:layout_constraintEnd_toEndOf="parent" android:textColor="#CCCCCC"
app:layout_constraintStart_toStartOf="parent" android:textSize="12sp" />
app:layout_constraintTop_toBottomOf="@id/playerOrOfflineBarrier" />
<com.google.android.material.textfield.TextInputLayout <TextView
android:id="@+id/rtmpLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/streamInfoTitle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/rtmpValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:hint="推流地址"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/copyRtmpButton" android:id="@+id/copyRtmpButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="复制地址" android:background="?attr/selectableItemBackground"
app:layout_constraintEnd_toEndOf="parent" android:padding="4dp"
app:layout_constraintTop_toBottomOf="@id/rtmpLayout" /> android:text="复制"
android:textColor="#4FC3F7"
android:textSize="12sp" />
</LinearLayout>
<androidx.constraintlayout.widget.Group <LinearLayout
android:id="@+id/rtmpAltGroup" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_marginTop="4dp"
android:visibility="gone" android:orientation="horizontal">
app:constraint_referenced_ids="rtmpAltLayout" />
<com.google.android.material.textfield.TextInputLayout <TextView
android:id="@+id/rtmpAltLayout" android:id="@+id/keyValue"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="10dp" android:layout_weight="1"
app:layout_constraintEnd_toEndOf="parent" android:ellipsize="middle"
app:layout_constraintStart_toStartOf="parent" android:singleLine="true"
app:layout_constraintTop_toBottomOf="@id/copyRtmpButton"> android:text="密钥: ..."
android:textColor="#CCCCCC"
android:textSize="12sp" />
<com.google.android.material.textfield.TextInputEditText <TextView
android:id="@+id/rtmpAltValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:hint="电脑本机 OBS 可用(等价地址)"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/keyLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rtmpAltLayout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/keyValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:hint="推流密钥"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/copyKeyButton" android:id="@+id/copyKeyButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="复制密钥" android:background="?attr/selectableItemBackground"
app:layout_constraintEnd_toEndOf="parent" android:padding="4dp"
app:layout_constraintTop_toBottomOf="@id/keyLayout" /> android:text="复制"
android:textColor="#4FC3F7"
android:textSize="12sp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton <TextView
android:id="@+id/deleteButton" android:id="@+id/deleteButton"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="18dp" android:layout_gravity="end"
android:text="删除房间" android:layout_marginTop="8dp"
android:visibility="gone" android:background="?attr/selectableItemBackground"
app:layout_constraintEnd_toEndOf="parent" android:padding="4dp"
app:layout_constraintStart_toStartOf="parent" android:text="删除房间"
app:layout_constraintTop_toBottomOf="@id/copyKeyButton" /> android:textColor="#FF6B6B"
android:textSize="12sp" />
</LinearLayout>
<!-- 聊天区域 --> <!-- 隐藏的旧控件(保持兼容性) -->
<TextView <com.google.android.material.textfield.TextInputLayout
android:id="@+id/chatTitle" android:id="@+id/rtmpLayout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginTop="18dp" android:visibility="gone"
android:text="💬 直播间聊天" app:layout_constraintStart_toStartOf="parent"
android:textSize="16sp" app:layout_constraintTop_toTopOf="parent" />
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/copyKeyButton" />
<androidx.recyclerview.widget.RecyclerView <com.google.android.material.textfield.TextInputLayout
android:id="@+id/chatRecyclerView" android:id="@+id/rtmpAltLayout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="200dp" android:layout_height="0dp"
android:layout_marginTop="8dp" android:visibility="gone"
android:background="#F5F5F5" app:layout_constraintStart_toStartOf="parent"
android:padding="8dp" app:layout_constraintTop_toTopOf="parent">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/chatTitle" />
<LinearLayout <com.google.android.material.textfield.TextInputEditText
android:id="@+id/chatInputLayout" android:id="@+id/rtmpAltValue"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:layout_marginTop="8dp" </com.google.android.material.textfield.TextInputLayout>
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/chatRecyclerView">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputLayout
android:id="@+id/chatInput" android:id="@+id/keyLayout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_weight="1" android:visibility="gone"
android:hint="发送弹幕..." app:layout_constraintStart_toStartOf="parent"
android:inputType="text" app:layout_constraintTop_toTopOf="parent" />
android:maxLines="1" />
<!-- 点赞按钮 --> <androidx.constraintlayout.widget.Group
<ImageButton android:id="@+id/rtmpAltGroup"
android:id="@+id/likeButton" android:layout_width="wrap_content"
android:layout_width="48dp" android:layout_height="wrap_content"
android:layout_height="48dp" android:visibility="gone" />
android:layout_marginStart="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_like_24"
android:contentDescription="点赞" />
<!-- 点赞数显示 -->
<TextView
android:id="@+id/likeCountText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:text="0"
android:textSize="14sp"
android:textColor="#666666" />
<com.google.android.material.button.MaterialButton
android:id="@+id/sendButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="发送" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,12 +5,12 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#000000"> android:background="#000000">
<!-- 全屏播放器容器 - 底层,视频填充整个屏幕 --> <!-- 播放器容器 - 居中显示 -->
<!-- 注意高度会根据设备类型动态调整电脑50%手机100% -->
<FrameLayout <FrameLayout
android:id="@+id/playerContainer" android:id="@+id/playerContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_gravity="center"
android:background="#000000" android:background="#000000"
android:tag="playerContainer" android:tag="playerContainer"
android:elevation="1dp"> android:elevation="1dp">
@ -29,18 +29,18 @@
android:layout_gravity="center" android:layout_gravity="center"
android:visibility="gone" /> android:visibility="gone" />
<!-- ExoPlayer 播放器 - 填充模式 --> <!-- ExoPlayer 播放器 - 保持比例 -->
<androidx.media3.ui.PlayerView <androidx.media3.ui.PlayerView
android:id="@+id/playerView" android:id="@+id/playerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
app:resize_mode="fill" app:resize_mode="fit"
app:use_controller="false" app:use_controller="false"
app:show_buffering="when_playing" app:show_buffering="when_playing"
app:surface_type="texture_view" /> app:surface_type="texture_view" />
<!-- GSYVideoPlayer 播放器视图 - 填充模式 --> <!-- GSYVideoPlayer 播放器视图 -->
<com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer <com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer
android:id="@+id/gsyPlayer" android:id="@+id/gsyPlayer"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -82,6 +82,32 @@
</LinearLayout> </LinearLayout>
<!-- 全屏按钮 - 视频区域右下角 -->
<ImageButton
android:id="@+id/fullscreenButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="end|bottom"
android:layout_margin="8dp"
android:background="@drawable/bg_circle_semi_transparent"
android:contentDescription="全屏"
android:src="@drawable/ic_fullscreen_24"
android:tint="@android:color/white"
android:visibility="visible" />
<!-- 退出全屏按钮 - 视频区域左上角(全屏时显示) -->
<ImageButton
android:id="@+id/exitFullscreenButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="start|top"
android:layout_margin="16dp"
android:background="@drawable/bg_circle_semi_transparent"
android:contentDescription="退出全屏"
android:src="@drawable/ic_arrow_back_24"
android:tint="@android:color/white"
android:visibility="gone" />
</FrameLayout> </FrameLayout>
<!-- UI覆盖层 - 悬浮在视频上方,透明背景 --> <!-- UI覆盖层 - 悬浮在视频上方,透明背景 -->
@ -124,17 +150,79 @@
android:src="@drawable/ic_arrow_back_24" android:src="@drawable/ic_arrow_back_24"
app:tint="@android:color/white" /> app:tint="@android:color/white" />
<View <!-- 主播信息卡片 - 放在顶部栏内 -->
<LinearLayout
android:id="@+id/roomInfoLayout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_weight="1" /> android:layout_weight="1"
android:layout_marginStart="8dp"
android:background="@drawable/bg_rounded_semi_transparent"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="8dp"
android:paddingVertical="6dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/streamerAvatar"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_person_24"
app:civ_border_color="#FFFFFF"
app:civ_border_width="1dp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/streamerName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="主播"
android:textColor="@android:color/white"
android:textSize="13sp"
android:textStyle="bold" />
<TextView
android:id="@+id/roomTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="120dp"
android:ellipsize="end"
android:maxLines="1"
android:text="直播间"
android:textColor="#CCFFFFFF"
android:textSize="11sp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/followButton"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="26dp"
android:layout_marginStart="4dp"
android:minWidth="48dp"
android:paddingHorizontal="10dp"
android:text="关注"
android:textColor="@android:color/white"
android:textSize="11sp"
android:textAllCaps="false"
app:backgroundTint="#FF4081"
app:cornerRadius="13dp" />
</LinearLayout>
<!-- 直播状态标签 --> <!-- 直播状态标签 -->
<TextView <TextView
android:id="@+id/liveTag" android:id="@+id/liveTag"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginStart="8dp"
android:background="@drawable/live_badge_background" android:background="@drawable/live_badge_background"
android:paddingHorizontal="8dp" android:paddingHorizontal="8dp"
android:paddingVertical="3dp" android:paddingVertical="3dp"
@ -148,6 +236,7 @@
android:id="@+id/topViewerLayout" android:id="@+id/topViewerLayout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/bg_rounded_semi_transparent" android:background="@drawable/bg_rounded_semi_transparent"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
@ -182,74 +271,6 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<!-- 主播信息卡片 - 悬浮在左上角 -->
<LinearLayout
android:id="@+id/roomInfoLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:background="@drawable/bg_rounded_semi_transparent"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="8dp"
android:paddingVertical="6dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topBar">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/streamerAvatar"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_person_24"
app:civ_border_color="#FFFFFF"
app:civ_border_width="1dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/streamerName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="主播"
android:textColor="@android:color/white"
android:textSize="13sp"
android:textStyle="bold" />
<TextView
android:id="@+id/roomTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="100dp"
android:ellipsize="end"
android:maxLines="1"
android:text="直播间"
android:textColor="#CCFFFFFF"
android:textSize="11sp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/followButton"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="26dp"
android:layout_marginStart="8dp"
android:minWidth="48dp"
android:paddingHorizontal="10dp"
android:text="关注"
android:textColor="@android:color/white"
android:textSize="11sp"
android:textAllCaps="false"
app:backgroundTint="#FF4081"
app:cornerRadius="13dp" />
</LinearLayout>
<!-- 底部渐变遮罩 --> <!-- 底部渐变遮罩 -->
<View <View
android:id="@+id/bottomGradient" android:id="@+id/bottomGradient"
@ -340,35 +361,6 @@
</LinearLayout> </LinearLayout>
<!-- 全屏按钮 - 右下角 -->
<ImageButton
android:id="@+id/fullscreenButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_circle_semi_transparent"
android:contentDescription="全屏"
android:src="@drawable/ic_fullscreen_24"
android:tint="@android:color/white"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/rightButtons"
app:layout_constraintEnd_toEndOf="parent" />
<!-- 退出全屏按钮 -->
<ImageButton
android:id="@+id/exitFullscreenButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_margin="12dp"
android:visibility="gone"
android:background="@drawable/bg_circle_semi_transparent"
android:contentDescription="退出全屏"
android:src="@drawable/ic_arrow_back_24"
android:tint="@android:color/white"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 底部输入栏 - 悬浮 --> <!-- 底部输入栏 - 悬浮 -->
<LinearLayout <LinearLayout
android:id="@+id/chatInputLayout" android:id="@+id/chatInputLayout"