diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts
index 48376a55..0efe7476 100644
--- a/android-app/app/build.gradle.kts
+++ b/android-app/app/build.gradle.kts
@@ -100,6 +100,14 @@ dependencies {
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.viewpager2:viewpager2:1.0.0")
+ // CameraX 相机库
+ val cameraxVersion = "1.3.1"
+ implementation("androidx.camera:camera-core:$cameraxVersion")
+ implementation("androidx.camera:camera-camera2:$cameraxVersion")
+ implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
+ implementation("androidx.camera:camera-video:$cameraxVersion")
+ implementation("androidx.camera:camera-view:$cameraxVersion")
+
implementation("com.github.bumptech.glide:glide:4.16.0")
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml
index 0963bab0..12b17e3e 100644
--- a/android-app/app/src/main/AndroidManifest.xml
+++ b/android-app/app/src/main/AndroidManifest.xml
@@ -204,7 +204,8 @@
+ android:screenOrientation="portrait"
+ android:windowSoftInputMode="adjustResize" />
+
+
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java b/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java
index c935c454..5f90a4fa 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java
@@ -110,6 +110,14 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
setupUI();
setupSurface();
+ // 接收传入的标题
+ String passedTitle = getIntent().getStringExtra("title");
+ if (!TextUtils.isEmpty(passedTitle)) {
+ binding.etTitle.setText(passedTitle);
+ // 如果有传入标题,隐藏标题输入卡片,让界面更简洁
+ binding.cardLiveInfo.setVisibility(View.GONE);
+ }
+
// 先检查主播资格,通过后再检查权限
checkStreamerStatus();
}
@@ -580,9 +588,16 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
String title = binding.etTitle.getText() != null ?
binding.etTitle.getText().toString().trim() : "";
+ // 如果输入框为空,尝试使用传入的标题
if (TextUtils.isEmpty(title)) {
- Toast.makeText(this, "请输入直播标题", Toast.LENGTH_SHORT).show();
- return;
+ String passedTitle = getIntent().getStringExtra("title");
+ if (!TextUtils.isEmpty(passedTitle)) {
+ title = passedTitle;
+ }
+ }
+
+ if (TextUtils.isEmpty(title)) {
+ title = "我的直播间"; // 默认标题
}
if (!cameraInitialized) {
diff --git a/android-app/app/src/main/java/com/example/livestreaming/CameraCaptureActivity.java b/android-app/app/src/main/java/com/example/livestreaming/CameraCaptureActivity.java
new file mode 100644
index 00000000..0c68064a
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/CameraCaptureActivity.java
@@ -0,0 +1,445 @@
+package com.example.livestreaming;
+
+import android.Manifest;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.video.MediaStoreOutputOptions;
+import androidx.camera.video.Quality;
+import androidx.camera.video.QualitySelector;
+import androidx.camera.video.Recorder;
+import androidx.camera.video.Recording;
+import androidx.camera.video.VideoCapture;
+import androidx.camera.video.VideoRecordEvent;
+import androidx.camera.view.PreviewView;
+import androidx.core.content.ContextCompat;
+
+import com.bumptech.glide.Glide;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class CameraCaptureActivity extends AppCompatActivity {
+
+ private static final String TAG = "CameraCaptureActivity";
+
+ private PreviewView cameraPreview;
+ private ImageView btnClose, ivGalleryThumbnail;
+ private View btnFlipCamera;
+ private LinearLayout btnGallery;
+ private FrameLayout btnCapture;
+ private View captureInner;
+ private TextView btnModePhoto, btnModeVideo, tvRecordTime;
+
+ private ProcessCameraProvider cameraProvider;
+ private ImageCapture imageCapture;
+ private VideoCapture videoCapture;
+ private Recording recording;
+
+ private boolean isPhotoMode = true;
+ private boolean isRecording = false;
+ private int lensFacing = CameraSelector.LENS_FACING_BACK;
+
+ private ExecutorService cameraExecutor;
+ private Handler handler = new Handler(Looper.getMainLooper());
+ private long recordStartTime;
+ private Runnable recordTimeRunnable;
+
+ private ActivityResultLauncher permissionLauncher;
+ private ActivityResultLauncher galleryLauncher;
+
+ public static void start(Context context) {
+ context.startActivity(new Intent(context, CameraCaptureActivity.class));
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_camera_capture);
+
+ initViews();
+ setupLaunchers();
+ setupClickListeners();
+
+ cameraExecutor = Executors.newSingleThreadExecutor();
+
+ if (checkPermissions()) {
+ startCamera();
+ } else {
+ requestPermissions();
+ }
+
+ loadLastGalleryImage();
+ }
+
+ private void initViews() {
+ cameraPreview = findViewById(R.id.cameraPreview);
+ btnClose = findViewById(R.id.btnClose);
+ btnFlipCamera = findViewById(R.id.btnFlipCamera);
+ btnGallery = findViewById(R.id.btnGallery);
+ ivGalleryThumbnail = btnGallery.findViewById(R.id.ivGalleryThumbnail);
+ btnCapture = findViewById(R.id.btnCapture);
+ captureInner = findViewById(R.id.captureInner);
+ btnModePhoto = findViewById(R.id.btnModePhoto);
+ btnModeVideo = findViewById(R.id.btnModeVideo);
+ tvRecordTime = findViewById(R.id.tvRecordTime);
+ }
+
+ private void setupLaunchers() {
+ permissionLauncher = registerForActivityResult(
+ new ActivityResultContracts.RequestMultiplePermissions(),
+ result -> {
+ boolean allGranted = true;
+ for (Boolean granted : result.values()) {
+ if (!granted) allGranted = false;
+ }
+ if (allGranted) {
+ startCamera();
+ } else {
+ Toast.makeText(this, "需要相机和存储权限", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+ );
+
+ galleryLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == RESULT_OK && result.getData() != null) {
+ Uri uri = result.getData().getData();
+ if (uri != null) {
+ goToPublishWork(uri);
+ }
+ }
+ }
+ );
+ }
+
+ private void setupClickListeners() {
+ btnClose.setOnClickListener(v -> finish());
+
+ btnFlipCamera.setOnClickListener(v -> {
+ lensFacing = (lensFacing == CameraSelector.LENS_FACING_BACK)
+ ? CameraSelector.LENS_FACING_FRONT
+ : CameraSelector.LENS_FACING_BACK;
+ startCamera();
+ });
+
+ btnGallery.setOnClickListener(v -> openGallery());
+
+ btnCapture.setOnClickListener(v -> {
+ if (isPhotoMode) {
+ takePhoto();
+ } else {
+ toggleVideoRecording();
+ }
+ });
+
+ btnModePhoto.setOnClickListener(v -> switchToPhotoMode());
+ btnModeVideo.setOnClickListener(v -> switchToVideoMode());
+
+ // 底部Tab点击事件
+ findViewById(R.id.tabDynamic).setOnClickListener(v -> {
+ // 跳转到发布中心的动态页面
+ Intent intent = new Intent(this, PublishCenterActivity.class);
+ intent.putExtra("tab", 0); // 动态
+ startActivity(intent);
+ finish();
+ });
+
+ // 相机Tab - 当前页面,不需要处理
+
+ findViewById(R.id.tabLive).setOnClickListener(v -> {
+ // 跳转到发布中心的开直播页面
+ Intent intent = new Intent(this, PublishCenterActivity.class);
+ intent.putExtra("tab", 2); // 开直播
+ startActivity(intent);
+ finish();
+ });
+ }
+
+ private boolean checkPermissions() {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private void requestPermissions() {
+ permissionLauncher.launch(new String[]{
+ Manifest.permission.CAMERA,
+ Manifest.permission.RECORD_AUDIO
+ });
+ }
+
+ private void startCamera() {
+ ListenableFuture future = ProcessCameraProvider.getInstance(this);
+ future.addListener(() -> {
+ try {
+ cameraProvider = future.get();
+ bindCameraUseCases();
+ } catch (ExecutionException | InterruptedException e) {
+ Log.e(TAG, "Camera init failed", e);
+ }
+ }, ContextCompat.getMainExecutor(this));
+ }
+
+ private void bindCameraUseCases() {
+ if (cameraProvider == null) return;
+
+ cameraProvider.unbindAll();
+
+ CameraSelector cameraSelector = new CameraSelector.Builder()
+ .requireLensFacing(lensFacing)
+ .build();
+
+ Preview preview = new Preview.Builder().build();
+ preview.setSurfaceProvider(cameraPreview.getSurfaceProvider());
+
+ imageCapture = new ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ .build();
+
+ Recorder recorder = new Recorder.Builder()
+ .setQualitySelector(QualitySelector.from(Quality.HD))
+ .build();
+ videoCapture = VideoCapture.withOutput(recorder);
+
+ try {
+ cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, videoCapture);
+ } catch (Exception e) {
+ Log.e(TAG, "Use case binding failed", e);
+ }
+ }
+
+ private void switchToPhotoMode() {
+ if (isPhotoMode) return;
+ isPhotoMode = true;
+
+ btnModePhoto.setBackgroundResource(R.drawable.bg_mode_selected);
+ btnModePhoto.setTextColor(0xFF333333); // 选中时黑色
+ btnModeVideo.setBackground(null);
+ btnModeVideo.setTextColor(0xFFFFFFFF); // 未选中时白色
+
+ captureInner.setBackgroundResource(R.drawable.bg_capture_inner);
+ captureInner.setLayoutParams(new FrameLayout.LayoutParams(
+ dpToPx(68), dpToPx(68), android.view.Gravity.CENTER));
+ }
+
+ private void switchToVideoMode() {
+ if (!isPhotoMode) return;
+ isPhotoMode = false;
+
+ btnModeVideo.setBackgroundResource(R.drawable.bg_mode_selected);
+ btnModeVideo.setTextColor(0xFF333333); // 选中时黑色
+ btnModePhoto.setBackground(null);
+ btnModePhoto.setTextColor(0xFFFFFFFF); // 未选中时白色
+
+ captureInner.setBackgroundResource(R.drawable.bg_capture_inner);
+ }
+
+ private void takePhoto() {
+ if (imageCapture == null) return;
+
+ String name = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
+ .format(System.currentTimeMillis());
+
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
+
+ ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(
+ getContentResolver(),
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ ).build();
+
+ imageCapture.takePicture(outputOptions, cameraExecutor,
+ new ImageCapture.OnImageSavedCallback() {
+ @Override
+ public void onImageSaved(@NonNull ImageCapture.OutputFileResults results) {
+ Uri savedUri = results.getSavedUri();
+ runOnUiThread(() -> {
+ Toast.makeText(CameraCaptureActivity.this, "照片已保存", Toast.LENGTH_SHORT).show();
+ if (savedUri != null) {
+ goToPublishWork(savedUri);
+ }
+ });
+ }
+
+ @Override
+ public void onError(@NonNull ImageCaptureException e) {
+ runOnUiThread(() ->
+ Toast.makeText(CameraCaptureActivity.this, "拍照失败", Toast.LENGTH_SHORT).show());
+ }
+ });
+ }
+
+ private void toggleVideoRecording() {
+ if (videoCapture == null) return;
+
+ if (isRecording) {
+ stopRecording();
+ } else {
+ startRecording();
+ }
+ }
+
+ private void startRecording() {
+ String name = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
+ .format(System.currentTimeMillis());
+
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
+
+ MediaStoreOutputOptions outputOptions = new MediaStoreOutputOptions.Builder(
+ getContentResolver(),
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ ).setContentValues(contentValues).build();
+
+ recording = videoCapture.getOutput()
+ .prepareRecording(this, outputOptions)
+ .withAudioEnabled()
+ .start(cameraExecutor, event -> {
+ if (event instanceof VideoRecordEvent.Start) {
+ runOnUiThread(() -> onRecordingStarted());
+ } else if (event instanceof VideoRecordEvent.Finalize) {
+ VideoRecordEvent.Finalize finalizeEvent = (VideoRecordEvent.Finalize) event;
+ Uri savedUri = finalizeEvent.getOutputResults().getOutputUri();
+ runOnUiThread(() -> onRecordingStopped(savedUri));
+ }
+ });
+ }
+
+ private void stopRecording() {
+ if (recording != null) {
+ recording.stop();
+ recording = null;
+ }
+ }
+
+ private void onRecordingStarted() {
+ isRecording = true;
+ recordStartTime = System.currentTimeMillis();
+
+ captureInner.setBackgroundResource(R.drawable.bg_capture_inner_recording);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ dpToPx(32), dpToPx(32), android.view.Gravity.CENTER);
+ captureInner.setLayoutParams(params);
+
+ tvRecordTime.setVisibility(View.VISIBLE);
+ startRecordTimer();
+ }
+
+ private void onRecordingStopped(Uri savedUri) {
+ isRecording = false;
+ stopRecordTimer();
+
+ captureInner.setBackgroundResource(R.drawable.bg_capture_inner);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ dpToPx(68), dpToPx(68), android.view.Gravity.CENTER);
+ captureInner.setLayoutParams(params);
+
+ tvRecordTime.setVisibility(View.GONE);
+
+ Toast.makeText(this, "视频已保存", Toast.LENGTH_SHORT).show();
+ if (savedUri != null) {
+ goToPublishWork(savedUri);
+ }
+ }
+
+ private void startRecordTimer() {
+ recordTimeRunnable = new Runnable() {
+ @Override
+ public void run() {
+ long elapsed = System.currentTimeMillis() - recordStartTime;
+ int seconds = (int) (elapsed / 1000);
+ int minutes = seconds / 60;
+ seconds = seconds % 60;
+ tvRecordTime.setText(String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
+ handler.postDelayed(this, 1000);
+ }
+ };
+ handler.post(recordTimeRunnable);
+ }
+
+ private void stopRecordTimer() {
+ if (recordTimeRunnable != null) {
+ handler.removeCallbacks(recordTimeRunnable);
+ }
+ }
+
+ private void openGallery() {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setType("image/* video/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
+ galleryLauncher.launch(intent);
+ }
+
+ private void loadLastGalleryImage() {
+ try {
+ String[] projection = {MediaStore.Images.Media._ID};
+ String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
+
+ Cursor cursor = getContentResolver().query(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ projection, null, null, sortOrder);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
+ Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
+ Glide.with(this).load(uri).centerCrop().into(ivGalleryThumbnail);
+ cursor.close();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Load gallery thumbnail failed", e);
+ }
+ }
+
+ private void goToPublishWork(Uri mediaUri) {
+ Intent intent = new Intent(this, PublishWorkActivity.class);
+ intent.putExtra("media_uri", mediaUri.toString());
+ startActivity(intent);
+ finish();
+ }
+
+ private int dpToPx(int dp) {
+ return (int) (dp * getResources().getDisplayMetrics().density);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ stopRecordTimer();
+ if (cameraExecutor != null) {
+ cameraExecutor.shutdown();
+ }
+ }
+}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java b/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java
index 39612e3f..dfe9a1b6 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java
@@ -1,20 +1,46 @@
package com.example.livestreaming;
+import android.Manifest;
import android.app.Activity;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.ClipData;
import android.content.ClipboardManager;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.MediaStore;
import android.text.TextUtils;
+import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.video.MediaStoreOutputOptions;
+import androidx.camera.video.Quality;
+import androidx.camera.video.QualitySelector;
+import androidx.camera.video.Recorder;
+import androidx.camera.video.Recording;
+import androidx.camera.video.VideoCapture;
+import androidx.camera.video.VideoRecordEvent;
+import androidx.camera.view.PreviewView;
+import androidx.core.content.ContextCompat;
import com.bumptech.glide.Glide;
import com.example.livestreaming.databinding.ActivityPublishCenterBinding;
@@ -26,13 +52,18 @@ import com.example.livestreaming.net.FileUploadResponse;
import com.example.livestreaming.net.Room;
import com.example.livestreaming.net.StreamUrls;
import com.example.livestreaming.net.WorksRequest;
-import com.google.android.material.tabs.TabLayout;
+import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
@@ -43,12 +74,33 @@ import retrofit2.Response;
public class PublishCenterActivity extends AppCompatActivity {
+ private static final String TAG = "PublishCenterActivity";
+
private ActivityPublishCenterBinding binding;
- private int currentTab = 0;
+ private int currentBottomTab = 2; // 0=动态, 1=相机, 2=开直播
+ private boolean isMobileLiveMode = true; // true=手机, false=电脑
private List categoryList = new ArrayList<>();
private int selectedCategoryId = -1;
private Uri dynamicCoverUri = null;
private ActivityResultLauncher imagePickerLauncher;
+
+ // 相机相关
+ private ProcessCameraProvider cameraProvider;
+ private int lensFacing = CameraSelector.LENS_FACING_FRONT; // 默认前置摄像头
+ private ActivityResultLauncher permissionLauncher;
+
+ // 相机拍摄相关
+ private ImageCapture imageCapture;
+ private VideoCapture videoCapture;
+ private Recording recording;
+ private boolean isCapturePhotoMode = true;
+ private boolean isRecording = false;
+ private int captureLensFacing = CameraSelector.LENS_FACING_FRONT; // 默认前置摄像头
+ private ExecutorService cameraExecutor;
+ private Handler handler = new Handler(Looper.getMainLooper());
+ private long recordStartTime;
+ private Runnable recordTimeRunnable;
+ private ActivityResultLauncher galleryLauncher;
public static void start(Context context) {
context.startActivity(new Intent(context, PublishCenterActivity.class));
@@ -64,11 +116,102 @@ public class PublishCenterActivity extends AppCompatActivity {
binding = ActivityPublishCenterBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setupImagePicker();
- setupToolbar();
- setupTabs();
+ setupPermissionLauncher();
+ setupGalleryLauncher();
setupClickListeners();
loadCategories();
- showTab(0);
+
+ cameraExecutor = Executors.newSingleThreadExecutor();
+
+ // 检查是否有传入的tab参数
+ int tabFromIntent = getIntent().getIntExtra("tab", 2);
+ showBottomTab(tabFromIntent);
+
+ // 检查相机权限并启动预览
+ checkCameraPermissionAndStart();
+ }
+
+ private void setupPermissionLauncher() {
+ permissionLauncher = registerForActivityResult(
+ new ActivityResultContracts.RequestMultiplePermissions(),
+ result -> {
+ Boolean cameraGranted = result.get(Manifest.permission.CAMERA);
+ if (cameraGranted != null && cameraGranted) {
+ startCamera();
+ if (currentBottomTab == 1) {
+ startCaptureCamera();
+ }
+ } else {
+ Toast.makeText(this, "需要相机权限才能预览", Toast.LENGTH_SHORT).show();
+ }
+ }
+ );
+ }
+
+ private void setupGalleryLauncher() {
+ galleryLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == RESULT_OK && result.getData() != null) {
+ Uri uri = result.getData().getData();
+ if (uri != null) {
+ goToPublishWork(uri);
+ }
+ }
+ }
+ );
+ }
+
+ private void checkCameraPermissionAndStart() {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED) {
+ startCamera();
+ } else {
+ permissionLauncher.launch(new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO});
+ }
+ }
+
+ private void startCamera() {
+ ListenableFuture future = ProcessCameraProvider.getInstance(this);
+ future.addListener(() -> {
+ try {
+ cameraProvider = future.get();
+ bindCameraPreview();
+ } catch (ExecutionException | InterruptedException e) {
+ Log.e(TAG, "Camera init failed", e);
+ }
+ }, ContextCompat.getMainExecutor(this));
+ }
+
+ private void bindCameraPreview() {
+ if (cameraProvider == null) return;
+
+ cameraProvider.unbindAll();
+
+ CameraSelector cameraSelector = new CameraSelector.Builder()
+ .requireLensFacing(lensFacing)
+ .build();
+
+ Preview preview = new Preview.Builder().build();
+ preview.setSurfaceProvider(binding.cameraPreview.getSurfaceProvider());
+
+ // 设置预览缩放类型为 FILL_CENTER(等比例放大填满,裁剪多余部分)
+ binding.cameraPreview.setScaleType(PreviewView.ScaleType.FILL_CENTER);
+ // 设置实现模式为兼容模式,确保正确裁剪
+ binding.cameraPreview.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
+
+ try {
+ cameraProvider.bindToLifecycle(this, cameraSelector, preview);
+ } catch (Exception e) {
+ Log.e(TAG, "Camera binding failed", e);
+ }
+ }
+
+ private void flipCamera() {
+ lensFacing = (lensFacing == CameraSelector.LENS_FACING_FRONT)
+ ? CameraSelector.LENS_FACING_BACK
+ : CameraSelector.LENS_FACING_FRONT;
+ bindCameraPreview();
}
private void setupImagePicker() {
@@ -86,44 +229,185 @@ public class PublishCenterActivity extends AppCompatActivity {
);
}
- private void setupToolbar() {
- binding.btnBack.setOnClickListener(v -> finish());
- }
-
- private void setupTabs() {
- binding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
- @Override
- public void onTabSelected(TabLayout.Tab tab) { showTab(tab.getPosition()); }
- @Override
- public void onTabUnselected(TabLayout.Tab tab) {}
- @Override
- public void onTabReselected(TabLayout.Tab tab) {}
- });
- }
-
private void setupClickListeners() {
+ // 关闭按钮
+ binding.btnBack.setOnClickListener(v -> finish());
+ binding.btnCloseDynamic.setOnClickListener(v -> finish());
+
+ // 手机/电脑切换
+ binding.btnModeMobile.setOnClickListener(v -> switchToMobileMode());
+ binding.btnModePc.setOnClickListener(v -> switchToPcMode());
+ binding.btnModeMobile2.setOnClickListener(v -> switchToMobileMode());
+ binding.btnModePc2.setOnClickListener(v -> switchToPcMode());
+ binding.btnBackPc.setOnClickListener(v -> finish());
+
+ // 翻转摄像头
+ binding.btnFlipCamera.setOnClickListener(v -> flipCamera());
+
+ // 底部Tab
+ binding.tabDynamic.setOnClickListener(v -> showBottomTab(0));
+ binding.tabCamera.setOnClickListener(v -> showBottomTab(1));
+ binding.tabLive.setOnClickListener(v -> showBottomTab(2));
+
+ // 相机拍摄相关
+ binding.btnCloseCamera.setOnClickListener(v -> finish());
+ binding.btnFlipCameraCapture.setOnClickListener(v -> flipCaptureCamera());
+ binding.btnCaptureModePhoto.setOnClickListener(v -> switchToCapturePhotoMode());
+ binding.btnCaptureModeVideo.setOnClickListener(v -> switchToCaptureVideoMode());
+ binding.btnCapturePhoto.setOnClickListener(v -> {
+ if (isCapturePhotoMode) {
+ takePhoto();
+ } else {
+ toggleVideoRecording();
+ }
+ });
+ binding.btnCaptureGallery.setOnClickListener(v -> openGallery());
+
+ // 手机直播
binding.btnStartLive.setOnClickListener(v -> startMobileLive());
- binding.btnPublishDynamic.setOnClickListener(v -> publishDynamic());
- binding.dynamicCoverContainer.setOnClickListener(v -> pickDynamicCover());
- binding.btnRemoveDynamicCover.setOnClickListener(v -> removeDynamicCover());
- binding.btnPublishWork.setOnClickListener(v -> PublishWorkActivity.start(this));
+
+ // 电脑直播
binding.btnCopyStreamUrl.setOnClickListener(v -> copyToClipboard("推流地址", binding.tvStreamUrl.getText().toString()));
binding.btnCopyStreamKey.setOnClickListener(v -> copyToClipboard("推流码", binding.tvStreamKey.getText().toString()));
binding.btnGetStreamInfo.setOnClickListener(v -> createPcLiveRoom());
+
+ // 发布动态
+ binding.btnPublishDynamic.setOnClickListener(v -> publishDynamic());
+ binding.dynamicCoverContainer.setOnClickListener(v -> pickDynamicCover());
+ binding.btnRemoveDynamicCover.setOnClickListener(v -> removeDynamicCover());
+ binding.btnAddImage.setOnClickListener(v -> {
+ binding.dynamicCoverContainer.setVisibility(View.VISIBLE);
+ pickDynamicCover();
+ });
}
- private void showTab(int position) {
- currentTab = position;
- binding.contentMobileLive.setVisibility(View.GONE);
- binding.contentDynamic.setVisibility(View.GONE);
- binding.contentWork.setVisibility(View.GONE);
- binding.contentPcLive.setVisibility(View.GONE);
- switch (position) {
- case 0: binding.contentMobileLive.setVisibility(View.VISIBLE); break;
- case 1: binding.contentDynamic.setVisibility(View.VISIBLE); break;
- case 2: binding.contentWork.setVisibility(View.VISIBLE); break;
- case 3: binding.contentPcLive.setVisibility(View.VISIBLE); break;
+ private void showBottomTab(int position) {
+ currentBottomTab = position;
+
+ // 切换tab时隐藏键盘
+ hideKeyboard();
+
+ // 根据tab切换底部导航栏样式
+ if (position == 1 || (position == 2 && isMobileLiveMode)) {
+ // 相机模式或手机直播模式:透明背景,白色字体
+ binding.bottomNav.setBackgroundColor(0x00000000); // 透明
+ binding.bottomNav.setElevation(0);
+ binding.tabDynamicText.setTextColor(0xFFFFFFFF); // 白色
+ binding.tabCameraText.setTextColor(0xFFFFFFFF);
+ binding.tabLiveText.setTextColor(0xFFFFFFFF);
+ } else {
+ // 其他模式:白色背景,正常颜色
+ binding.bottomNav.setBackgroundColor(0xFFFFFFFF); // 白色
+ binding.bottomNav.setElevation(8 * getResources().getDisplayMetrics().density);
+ binding.tabDynamicText.setTextColor(position == 0 ? 0xFFFF4757 : 0xFF999999);
+ binding.tabCameraText.setTextColor(0xFF999999);
+ binding.tabLiveText.setTextColor(position == 2 ? 0xFFFF4757 : 0xFF999999);
}
+
+ // 更新选中状态的粗体
+ binding.tabDynamicText.setTypeface(null, position == 0 ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL);
+ binding.tabCameraText.setTypeface(null, position == 1 ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL);
+ binding.tabLiveText.setTypeface(null, position == 2 ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL);
+
+ // 显示对应内容
+ binding.contentDynamic.setVisibility(position == 0 ? View.VISIBLE : View.GONE);
+ binding.contentCamera.setVisibility(position == 1 ? View.VISIBLE : View.GONE);
+ binding.contentLive.setVisibility(position == 2 ? View.VISIBLE : View.GONE);
+
+ // 根据tab启动对应的相机
+ if (position == 0) {
+ // 自动聚焦输入框
+ binding.etDynamicContent.postDelayed(() -> {
+ binding.etDynamicContent.requestFocus();
+ android.view.inputmethod.InputMethodManager imm =
+ (android.view.inputmethod.InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(binding.etDynamicContent, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT);
+ }
+ }, 200);
+ } else if (position == 1) {
+ // 启动作品拍摄相机预览
+ startCaptureCamera();
+ loadLastGalleryImage();
+ } else if (position == 2 && isMobileLiveMode) {
+ // 启动开直播相机预览
+ startCamera();
+ }
+ }
+
+ private void hideKeyboard() {
+ View view = getCurrentFocus();
+ if (view != null) {
+ android.view.inputmethod.InputMethodManager imm =
+ (android.view.inputmethod.InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ view.clearFocus();
+ }
+ }
+
+ private void switchToMobileMode() {
+ if (isMobileLiveMode) return;
+ isMobileLiveMode = true;
+
+ // 更新手机模式顶部栏的切换按钮
+ binding.btnModeMobile.setBackgroundResource(R.drawable.bg_mode_selected);
+ binding.btnModeMobile.setTextColor(0xFF333333);
+ binding.btnModePc.setBackground(null);
+ binding.btnModePc.setTextColor(0xFFFFFFFF);
+
+ // 更新电脑模式顶部栏的切换按钮
+ binding.btnModeMobile2.setBackground(null);
+ binding.btnModeMobile2.setTextColor(0xFF666666);
+ binding.btnModePc2.setBackgroundResource(R.drawable.bg_mode_selected_dark);
+ binding.btnModePc2.setTextColor(0xFFFFFFFF);
+
+ binding.contentMobileLive.setVisibility(View.VISIBLE);
+ binding.contentPcLive.setVisibility(View.GONE);
+ binding.pcTopBar.setVisibility(View.GONE);
+ binding.bottomNav.setVisibility(View.VISIBLE);
+
+ // 手机直播模式:透明背景,白色字体(和相机页面一样)
+ binding.bottomNav.setBackgroundColor(0x00000000);
+ binding.bottomNav.setElevation(0);
+ binding.tabDynamicText.setTextColor(0xFFFFFFFF);
+ binding.tabCameraText.setTextColor(0xFFFFFFFF);
+ binding.tabLiveText.setTextColor(0xFFFFFFFF);
+ binding.tabLiveText.setTypeface(null, android.graphics.Typeface.BOLD);
+
+ // 启动相机预览
+ startCamera();
+ }
+
+ private void switchToPcMode() {
+ if (!isMobileLiveMode) return;
+ isMobileLiveMode = false;
+
+ // 更新手机模式顶部栏的切换按钮
+ binding.btnModePc.setBackgroundResource(R.drawable.bg_mode_selected);
+ binding.btnModePc.setTextColor(0xFF333333);
+ binding.btnModeMobile.setBackground(null);
+ binding.btnModeMobile.setTextColor(0xFFFFFFFF);
+
+ // 更新电脑模式顶部栏的切换按钮
+ binding.btnModePc2.setBackgroundResource(R.drawable.bg_mode_selected_dark);
+ binding.btnModePc2.setTextColor(0xFFFFFFFF);
+ binding.btnModeMobile2.setBackground(null);
+ binding.btnModeMobile2.setTextColor(0xFF666666);
+
+ binding.contentMobileLive.setVisibility(View.GONE);
+ binding.contentPcLive.setVisibility(View.VISIBLE);
+ binding.pcTopBar.setVisibility(View.VISIBLE);
+ binding.bottomNav.setVisibility(View.VISIBLE);
+
+ // 电脑直播模式:白色背景,正常颜色
+ binding.bottomNav.setBackgroundColor(0xFFFFFFFF);
+ binding.bottomNav.setElevation(8 * getResources().getDisplayMetrics().density);
+ binding.tabDynamicText.setTextColor(0xFF999999);
+ binding.tabCameraText.setTextColor(0xFF999999);
+ binding.tabLiveText.setTextColor(0xFFFF4757);
+ binding.tabLiveText.setTypeface(null, android.graphics.Typeface.BOLD);
}
private void loadCategories() {
@@ -203,11 +487,8 @@ public class PublishCenterActivity extends AppCompatActivity {
binding.btnPublishDynamic.setText("发布中...");
if (dynamicCoverUri != null) {
- // 用户选择了封面,上传后发布
uploadDynamicCoverAndPublish(content);
} else {
- // 用户没选封面,使用特殊标记(纯文字动态)
- // 使用 "text_only" 作为标记,在展示时识别为纯文字动态
doPublishDynamic(content, "text_only");
}
}
@@ -253,7 +534,6 @@ public class PublishCenterActivity extends AppCompatActivity {
request.setDescription(content);
request.setType("IMAGE");
request.setImageUrls(new ArrayList<>());
- // 如果没有封面,设置特殊标识URL,表示这是纯文字动态
if (coverUrl == null || coverUrl.isEmpty()) {
request.setCoverUrl("TEXT_ONLY_DYNAMIC_PLACEHOLDER");
} else {
@@ -283,7 +563,7 @@ public class PublishCenterActivity extends AppCompatActivity {
private void resetDynamicButton() {
binding.btnPublishDynamic.setEnabled(true);
- binding.btnPublishDynamic.setText("发布动态");
+ binding.btnPublishDynamic.setText("发布");
}
private File uriToFile(Uri uri) {
@@ -317,10 +597,9 @@ public class PublishCenterActivity extends AppCompatActivity {
binding.btnGetStreamInfo.setText("创建中...");
binding.streamInfoLoading.setVisibility(View.VISIBLE);
- // 获取当前用户昵称作为主播名称
String streamerName = com.example.livestreaming.net.AuthStore.getNickname(this);
if (TextUtils.isEmpty(streamerName)) {
- streamerName = title; // 如果没有昵称,使用标题
+ streamerName = title;
}
CreateRoomRequest request = new CreateRoomRequest(title, streamerName, "pc");
@@ -375,4 +654,282 @@ public class PublishCenterActivity extends AppCompatActivity {
Toast.makeText(this, label + "已复制", Toast.LENGTH_SHORT).show();
}
}
+
+ // ========== 相机拍摄相关方法 ==========
+
+ private void startCaptureCamera() {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+
+ ListenableFuture future = ProcessCameraProvider.getInstance(this);
+ future.addListener(() -> {
+ try {
+ cameraProvider = future.get();
+ bindCaptureCameraUseCases();
+ } catch (ExecutionException | InterruptedException e) {
+ Log.e(TAG, "Camera init failed", e);
+ }
+ }, ContextCompat.getMainExecutor(this));
+ }
+
+ private void bindCaptureCameraUseCases() {
+ if (cameraProvider == null) return;
+
+ cameraProvider.unbindAll();
+
+ CameraSelector cameraSelector = new CameraSelector.Builder()
+ .requireLensFacing(captureLensFacing)
+ .build();
+
+ Preview preview = new Preview.Builder().build();
+ preview.setSurfaceProvider(binding.cameraCapturePreview.getSurfaceProvider());
+
+ binding.cameraCapturePreview.setScaleType(PreviewView.ScaleType.FILL_CENTER);
+ binding.cameraCapturePreview.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
+
+ imageCapture = new ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ .build();
+
+ Recorder recorder = new Recorder.Builder()
+ .setQualitySelector(QualitySelector.from(Quality.HD))
+ .build();
+ videoCapture = VideoCapture.withOutput(recorder);
+
+ try {
+ cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, videoCapture);
+ } catch (Exception e) {
+ Log.e(TAG, "Use case binding failed", e);
+ }
+ }
+
+ private void flipCaptureCamera() {
+ captureLensFacing = (captureLensFacing == CameraSelector.LENS_FACING_BACK)
+ ? CameraSelector.LENS_FACING_FRONT
+ : CameraSelector.LENS_FACING_BACK;
+ bindCaptureCameraUseCases();
+ }
+
+ private void switchToCapturePhotoMode() {
+ if (isCapturePhotoMode) return;
+ isCapturePhotoMode = true;
+
+ binding.btnCaptureModePhoto.setBackgroundResource(R.drawable.bg_mode_selected);
+ binding.btnCaptureModePhoto.setTextColor(0xFF333333);
+ binding.btnCaptureModeVideo.setBackground(null);
+ binding.btnCaptureModeVideo.setTextColor(0xFFFFFFFF);
+
+ binding.captureInnerView.setBackgroundResource(R.drawable.bg_capture_inner);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ dpToPx(68), dpToPx(68), android.view.Gravity.CENTER);
+ binding.captureInnerView.setLayoutParams(params);
+ }
+
+ private void switchToCaptureVideoMode() {
+ if (!isCapturePhotoMode) return;
+ isCapturePhotoMode = false;
+
+ binding.btnCaptureModeVideo.setBackgroundResource(R.drawable.bg_mode_selected);
+ binding.btnCaptureModeVideo.setTextColor(0xFF333333);
+ binding.btnCaptureModePhoto.setBackground(null);
+ binding.btnCaptureModePhoto.setTextColor(0xFFFFFFFF);
+
+ binding.captureInnerView.setBackgroundResource(R.drawable.bg_capture_inner);
+ }
+
+ private void takePhoto() {
+ if (imageCapture == null) return;
+
+ String name = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
+ .format(System.currentTimeMillis());
+
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
+
+ ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(
+ getContentResolver(),
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ ).build();
+
+ imageCapture.takePicture(outputOptions, cameraExecutor,
+ new ImageCapture.OnImageSavedCallback() {
+ @Override
+ public void onImageSaved(@NonNull ImageCapture.OutputFileResults results) {
+ Uri savedUri = results.getSavedUri();
+ runOnUiThread(() -> {
+ Toast.makeText(PublishCenterActivity.this, "照片已保存", Toast.LENGTH_SHORT).show();
+ if (savedUri != null) {
+ goToPublishWork(savedUri);
+ }
+ });
+ }
+
+ @Override
+ public void onError(@NonNull ImageCaptureException e) {
+ runOnUiThread(() ->
+ Toast.makeText(PublishCenterActivity.this, "拍照失败", Toast.LENGTH_SHORT).show());
+ }
+ });
+ }
+
+ private void toggleVideoRecording() {
+ if (videoCapture == null) return;
+
+ if (isRecording) {
+ stopRecording();
+ } else {
+ startRecording();
+ }
+ }
+
+ private void startRecording() {
+ String name = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
+ .format(System.currentTimeMillis());
+
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
+
+ MediaStoreOutputOptions outputOptions = new MediaStoreOutputOptions.Builder(
+ getContentResolver(),
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ ).setContentValues(contentValues).build();
+
+ recording = videoCapture.getOutput()
+ .prepareRecording(this, outputOptions)
+ .withAudioEnabled()
+ .start(cameraExecutor, event -> {
+ if (event instanceof VideoRecordEvent.Start) {
+ runOnUiThread(() -> onRecordingStarted());
+ } else if (event instanceof VideoRecordEvent.Finalize) {
+ VideoRecordEvent.Finalize finalizeEvent = (VideoRecordEvent.Finalize) event;
+ Uri savedUri = finalizeEvent.getOutputResults().getOutputUri();
+ runOnUiThread(() -> onRecordingStopped(savedUri));
+ }
+ });
+ }
+
+ private void stopRecording() {
+ if (recording != null) {
+ recording.stop();
+ recording = null;
+ }
+ }
+
+ private void onRecordingStarted() {
+ isRecording = true;
+ recordStartTime = System.currentTimeMillis();
+
+ binding.captureInnerView.setBackgroundResource(R.drawable.bg_capture_inner_recording);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ dpToPx(32), dpToPx(32), android.view.Gravity.CENTER);
+ binding.captureInnerView.setLayoutParams(params);
+
+ binding.tvCaptureRecordTime.setVisibility(View.VISIBLE);
+ startRecordTimer();
+ }
+
+ private void onRecordingStopped(Uri savedUri) {
+ isRecording = false;
+ stopRecordTimer();
+
+ binding.captureInnerView.setBackgroundResource(R.drawable.bg_capture_inner);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ dpToPx(68), dpToPx(68), android.view.Gravity.CENTER);
+ binding.captureInnerView.setLayoutParams(params);
+
+ binding.tvCaptureRecordTime.setVisibility(View.GONE);
+
+ Toast.makeText(this, "视频已保存", Toast.LENGTH_SHORT).show();
+ if (savedUri != null) {
+ goToPublishWork(savedUri);
+ }
+ }
+
+ private void startRecordTimer() {
+ recordTimeRunnable = new Runnable() {
+ @Override
+ public void run() {
+ long elapsed = System.currentTimeMillis() - recordStartTime;
+ int seconds = (int) (elapsed / 1000);
+ int minutes = seconds / 60;
+ seconds = seconds % 60;
+ binding.tvCaptureRecordTime.setText(String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds));
+ handler.postDelayed(this, 1000);
+ }
+ };
+ handler.post(recordTimeRunnable);
+ }
+
+ private void stopRecordTimer() {
+ if (recordTimeRunnable != null) {
+ handler.removeCallbacks(recordTimeRunnable);
+ }
+ }
+
+ private void openGallery() {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setType("image/* video/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
+ galleryLauncher.launch(intent);
+ }
+
+ private void loadLastGalleryImage() {
+ try {
+ String[] projection = {MediaStore.Images.Media._ID};
+ String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
+
+ Cursor cursor = getContentResolver().query(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ projection, null, null, sortOrder);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
+ Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
+ Glide.with(this).load(uri).centerCrop().into(binding.ivCaptureGalleryThumbnail);
+ cursor.close();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Load gallery thumbnail failed", e);
+ }
+ }
+
+ private void goToPublishWork(Uri mediaUri) {
+ Intent intent = new Intent(this, PublishWorkActivity.class);
+ intent.putExtra("media_uri", mediaUri.toString());
+ startActivity(intent);
+ }
+
+ private int dpToPx(int dp) {
+ return (int) (dp * getResources().getDisplayMetrics().density);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 恢复相机预览
+ if (currentBottomTab == 2 && isMobileLiveMode) {
+ // 开直播手机模式,重新绑定相机预览
+ startCamera();
+ } else if (currentBottomTab == 1) {
+ // 作品拍摄模式,重新绑定相机
+ startCaptureCamera();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ stopRecordTimer();
+ if (cameraProvider != null) {
+ cameraProvider.unbindAll();
+ }
+ if (cameraExecutor != null) {
+ cameraExecutor.shutdown();
+ }
+ }
}
diff --git a/android-app/app/src/main/res/drawable/bg_camera_bottom_gradient.xml b/android-app/app/src/main/res/drawable/bg_camera_bottom_gradient.xml
new file mode 100644
index 00000000..6d75b0b3
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_camera_bottom_gradient.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_capture_inner.xml b/android-app/app/src/main/res/drawable/bg_capture_inner.xml
new file mode 100644
index 00000000..9f3c01df
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_capture_inner.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_capture_inner_recording.xml b/android-app/app/src/main/res/drawable/bg_capture_inner_recording.xml
new file mode 100644
index 00000000..ef8200c3
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_capture_inner_recording.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_capture_outer.xml b/android-app/app/src/main/res/drawable/bg_capture_outer.xml
new file mode 100644
index 00000000..6d9a85f2
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_capture_outer.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_edit_text_light.xml b/android-app/app/src/main/res/drawable/bg_edit_text_light.xml
new file mode 100644
index 00000000..0ce3b003
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_edit_text_light.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_gallery_thumbnail.xml b/android-app/app/src/main/res/drawable/bg_gallery_thumbnail.xml
new file mode 100644
index 00000000..f8bffa63
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_gallery_thumbnail.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_title_input.xml b/android-app/app/src/main/res/drawable/bg_live_title_input.xml
new file mode 100644
index 00000000..1de39bf3
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_live_title_input.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_mode_selected.xml b/android-app/app/src/main/res/drawable/bg_mode_selected.xml
new file mode 100644
index 00000000..38e2b493
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_mode_selected.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_mode_selected_dark.xml b/android-app/app/src/main/res/drawable/bg_mode_selected_dark.xml
new file mode 100644
index 00000000..20ee8123
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_mode_selected_dark.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_mode_selector.xml b/android-app/app/src/main/res/drawable/bg_mode_selector.xml
new file mode 100644
index 00000000..7a327bee
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_mode_selector.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_mode_selector_light.xml b/android-app/app/src/main/res/drawable/bg_mode_selector_light.xml
new file mode 100644
index 00000000..ca3baed0
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_mode_selector_light.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_publish_button.xml b/android-app/app/src/main/res/drawable/bg_publish_button.xml
index ef8200c3..57eb3335 100644
--- a/android-app/app/src/main/res/drawable/bg_publish_button.xml
+++ b/android-app/app/src/main/res/drawable/bg_publish_button.xml
@@ -1,6 +1,21 @@
-
-
-
-
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_record_time.xml b/android-app/app/src/main/res/drawable/bg_record_time.xml
new file mode 100644
index 00000000..7b3b0040
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_record_time.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_spinner_light.xml b/android-app/app/src/main/res/drawable/bg_spinner_light.xml
new file mode 100644
index 00000000..0ce3b003
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_spinner_light.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/ic_flip_camera_24.xml b/android-app/app/src/main/res/drawable/ic_flip_camera_24.xml
new file mode 100644
index 00000000..d3bc77b5
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/ic_flip_camera_24.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/ic_photo_library_24.xml b/android-app/app/src/main/res/drawable/ic_photo_library_24.xml
new file mode 100644
index 00000000..00e3da54
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/ic_photo_library_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_camera_capture.xml b/android-app/app/src/main/res/layout/activity_camera_capture.xml
new file mode 100644
index 00000000..bc14e9f5
--- /dev/null
+++ b/android-app/app/src/main/res/layout/activity_camera_capture.xml
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_publish_center.xml b/android-app/app/src/main/res/layout/activity_publish_center.xml
index 57a868f4..ea930a3e 100644
--- a/android-app/app/src/main/res/layout/activity_publish_center.xml
+++ b/android-app/app/src/main/res/layout/activity_publish_center.xml
@@ -1,131 +1,534 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ android:background="#000000"
+ android:visibility="gone">
-
-
+
+ android:layout_height="match_parent" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:gravity="center">
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:orientation="horizontal"
+ android:background="@drawable/bg_mode_selector"
+ android:padding="3dp">
-
+ android:paddingHorizontal="16dp"
+ android:paddingVertical="6dp"
+ android:text="手机"
+ android:textSize="13sp"
+ android:textColor="#333333"
+ android:background="@drawable/bg_mode_selected" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ android:padding="20dp"
+ android:paddingTop="72dp">
-
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
+ app:boxStrokeColor="#FF4757"
+ app:hintTextColor="#999999">
+ android:maxLength="30"
+ android:textColor="#333333" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+