主题:添加样式

This commit is contained in:
xiao12feng8 2026-01-11 12:07:48 +08:00
parent c7eac54c04
commit b02fdf2ea7
23 changed files with 2144 additions and 325 deletions

View File

@ -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")

View File

@ -204,7 +204,8 @@
<activity
android:name="com.example.livestreaming.PublishCenterActivity"
android:exported="false"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="com.example.livestreaming.WishTreeActivity"
@ -357,6 +358,13 @@
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<!-- 相机拍摄Activity -->
<activity
android:name="com.example.livestreaming.CameraCaptureActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.NoActionBar" />
</application>
</manifest>

View File

@ -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) {

View File

@ -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<Recorder> 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<String[]> permissionLauncher;
private ActivityResultLauncher<Intent> 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<ProcessCameraProvider> 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();
}
}
}

View File

@ -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<CategoryResponse> categoryList = new ArrayList<>();
private int selectedCategoryId = -1;
private Uri dynamicCoverUri = null;
private ActivityResultLauncher<Intent> imagePickerLauncher;
// 相机相关
private ProcessCameraProvider cameraProvider;
private int lensFacing = CameraSelector.LENS_FACING_FRONT; // 默认前置摄像头
private ActivityResultLauncher<String[]> permissionLauncher;
// 相机拍摄相关
private ImageCapture imageCapture;
private VideoCapture<Recorder> 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<Intent> 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<ProcessCameraProvider> 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<ProcessCameraProvider> 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();
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#00000000"
android:endColor="#99000000"
android:angle="270" />
</shape>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F5F5F5" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
</shape>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FF4757" />
<corners android:radius="8dp" />
</shape>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#E53E4F" />
<corners android:radius="16dp" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="rectangle">
<solid android:color="#FFCDD2" />
<corners android:radius="16dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#FF4757" />
<corners android:radius="16dp" />
</shape>
</item>
</selector>

View File

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

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F5F5F5" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
</shape>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M9,12c0,1.66 1.34,3 3,3s3,-1.34 3,-3s-1.34,-3 -3,-3S9,10.34 9,12zM7,12c0,-2.76 2.24,-5 5,-5s5,2.24 5,5s-2.24,5 -5,5S7,14.76 7,12zM20,5h-3.17L15,3H9L7.17,5H4C2.9,5 2,5.9 2,7v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V7C22,5.9 21.1,5 20,5zM20,19H4V7h4.05l1.83,-2h4.24l1.83,2H20V19z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M12,8.5L12,6L8,9.5L12,13L12,10.5C14.21,10.5 16,12.29 16,14.5C16,15.32 15.75,16.08 15.33,16.71L16.44,17.82C17.11,16.89 17.5,15.74 17.5,14.5C17.5,11.46 15.04,9 12,9L12,8.5Z"/>
</vector>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M22,16V4c0,-1.1 -0.9,-2 -2,-2H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zm-11,-4l2.03,2.71L16,11l4,5H8l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2H4V6H2z"/>
</vector>

View File

@ -0,0 +1,276 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<!-- 相机预览 -->
<androidx.camera.view.PreviewView
android:id="@+id/cameraPreview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- 顶部工具栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp">
<!-- 关闭按钮 -->
<ImageView
android:id="@+id/btnClose"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_close_24"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="关闭"
app:tint="@android:color/white" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 翻转摄像头 -->
<LinearLayout
android:id="@+id/btnFlipCamera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="8dp"
android:background="?attr/selectableItemBackgroundBorderless">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:src="@drawable/ic_flip_camera_24"
android:contentDescription="翻转"
app:tint="@android:color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="翻转"
android:textColor="@android:color/white"
android:textSize="10sp" />
</LinearLayout>
</LinearLayout>
<!-- 底部控制区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical"
android:paddingBottom="56dp"
android:background="@drawable/bg_camera_bottom_gradient">
<!-- 模式切换:照片/视频 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="32dp"
android:orientation="horizontal"
android:background="@drawable/bg_mode_selector"
android:padding="4dp">
<TextView
android:id="@+id/btnModePhoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingVertical="10dp"
android:text="照片"
android:textSize="15sp"
android:textColor="#333333"
android:textStyle="bold"
android:background="@drawable/bg_mode_selected" />
<TextView
android:id="@+id/btnModeVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingVertical="10dp"
android:text="视频"
android:textSize="15sp"
android:textColor="#FFFFFF" />
</LinearLayout>
<!-- 拍照控制区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="48dp">
<!-- 左侧占位 -->
<View
android:layout_width="56dp"
android:layout_height="56dp" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 拍照按钮 - 更大 -->
<FrameLayout
android:id="@+id/btnCapture"
android:layout_width="88dp"
android:layout_height="88dp">
<!-- 外圈 -->
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_capture_outer" />
<!-- 内圈 -->
<View
android:id="@+id/captureInner"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_gravity="center"
android:background="@drawable/bg_capture_inner" />
</FrameLayout>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 相册入口 - 放到右侧,变小 -->
<LinearLayout
android:id="@+id/btnGallery"
android:layout_width="56dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
<FrameLayout
android:layout_width="44dp"
android:layout_height="44dp"
android:background="@drawable/bg_gallery_thumbnail">
<ImageView
android:id="@+id/ivGalleryThumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="相册"
android:textColor="@android:color/white"
android:textSize="10sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- 底部导航栏 - 透明背景 -->
<LinearLayout
android:id="@+id/bottomNav"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_gravity="bottom"
android:orientation="horizontal"
android:background="@android:color/transparent">
<!-- 动态 -->
<LinearLayout
android:id="@+id/tabDynamic"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/tabDynamicText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="动态"
android:textSize="14sp"
android:textColor="@android:color/white" />
</LinearLayout>
<!-- 相机 -->
<LinearLayout
android:id="@+id/tabCamera"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/tabCameraText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="相机"
android:textSize="14sp"
android:textColor="@android:color/white"
android:textStyle="bold" />
</LinearLayout>
<!-- 开直播 -->
<LinearLayout
android:id="@+id/tabLive"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/tabLiveText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开直播"
android:textSize="14sp"
android:textColor="@android:color/white" />
</LinearLayout>
</LinearLayout>
<!-- 录制时间显示 -->
<TextView
android:id="@+id/tvRecordTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal"
android:layout_marginTop="80dp"
android:paddingHorizontal="16dp"
android:paddingVertical="6dp"
android:background="@drawable/bg_record_time"
android:text="00:00"
android:textColor="@android:color/white"
android:textSize="14sp"
android:visibility="gone" />
</FrameLayout>