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" /> - + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + +