主题:添加样式
This commit is contained in:
parent
c7eac54c04
commit
b02fdf2ea7
|
|
@ -100,6 +100,14 @@ dependencies {
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.viewpager2:viewpager2:1.0.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")
|
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||||
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
|
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,8 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="com.example.livestreaming.PublishCenterActivity"
|
android:name="com.example.livestreaming.PublishCenterActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.example.livestreaming.WishTreeActivity"
|
android:name="com.example.livestreaming.WishTreeActivity"
|
||||||
|
|
@ -357,6 +358,13 @@
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
|
<!-- 相机拍摄Activity -->
|
||||||
|
<activity
|
||||||
|
android:name="com.example.livestreaming.CameraCaptureActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,14 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
|
||||||
setupUI();
|
setupUI();
|
||||||
setupSurface();
|
setupSurface();
|
||||||
|
|
||||||
|
// 接收传入的标题
|
||||||
|
String passedTitle = getIntent().getStringExtra("title");
|
||||||
|
if (!TextUtils.isEmpty(passedTitle)) {
|
||||||
|
binding.etTitle.setText(passedTitle);
|
||||||
|
// 如果有传入标题,隐藏标题输入卡片,让界面更简洁
|
||||||
|
binding.cardLiveInfo.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
// 先检查主播资格,通过后再检查权限
|
// 先检查主播资格,通过后再检查权限
|
||||||
checkStreamerStatus();
|
checkStreamerStatus();
|
||||||
}
|
}
|
||||||
|
|
@ -580,9 +588,16 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
|
||||||
String title = binding.etTitle.getText() != null ?
|
String title = binding.etTitle.getText() != null ?
|
||||||
binding.etTitle.getText().toString().trim() : "";
|
binding.etTitle.getText().toString().trim() : "";
|
||||||
|
|
||||||
|
// 如果输入框为空,尝试使用传入的标题
|
||||||
if (TextUtils.isEmpty(title)) {
|
if (TextUtils.isEmpty(title)) {
|
||||||
Toast.makeText(this, "请输入直播标题", Toast.LENGTH_SHORT).show();
|
String passedTitle = getIntent().getStringExtra("title");
|
||||||
return;
|
if (!TextUtils.isEmpty(passedTitle)) {
|
||||||
|
title = passedTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(title)) {
|
||||||
|
title = "我的直播间"; // 默认标题
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cameraInitialized) {
|
if (!cameraInitialized) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,46 @@
|
||||||
package com.example.livestreaming;
|
package com.example.livestreaming;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.provider.MediaStore;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
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.bumptech.glide.Glide;
|
||||||
import com.example.livestreaming.databinding.ActivityPublishCenterBinding;
|
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.Room;
|
||||||
import com.example.livestreaming.net.StreamUrls;
|
import com.example.livestreaming.net.StreamUrls;
|
||||||
import com.example.livestreaming.net.WorksRequest;
|
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.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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.MediaType;
|
||||||
import okhttp3.MultipartBody;
|
import okhttp3.MultipartBody;
|
||||||
|
|
@ -43,13 +74,34 @@ import retrofit2.Response;
|
||||||
|
|
||||||
public class PublishCenterActivity extends AppCompatActivity {
|
public class PublishCenterActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final String TAG = "PublishCenterActivity";
|
||||||
|
|
||||||
private ActivityPublishCenterBinding binding;
|
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 List<CategoryResponse> categoryList = new ArrayList<>();
|
||||||
private int selectedCategoryId = -1;
|
private int selectedCategoryId = -1;
|
||||||
private Uri dynamicCoverUri = null;
|
private Uri dynamicCoverUri = null;
|
||||||
private ActivityResultLauncher<Intent> imagePickerLauncher;
|
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) {
|
public static void start(Context context) {
|
||||||
context.startActivity(new Intent(context, PublishCenterActivity.class));
|
context.startActivity(new Intent(context, PublishCenterActivity.class));
|
||||||
}
|
}
|
||||||
|
|
@ -64,11 +116,102 @@ public class PublishCenterActivity extends AppCompatActivity {
|
||||||
binding = ActivityPublishCenterBinding.inflate(getLayoutInflater());
|
binding = ActivityPublishCenterBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
setupImagePicker();
|
setupImagePicker();
|
||||||
setupToolbar();
|
setupPermissionLauncher();
|
||||||
setupTabs();
|
setupGalleryLauncher();
|
||||||
setupClickListeners();
|
setupClickListeners();
|
||||||
loadCategories();
|
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() {
|
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() {
|
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.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.btnCopyStreamUrl.setOnClickListener(v -> copyToClipboard("推流地址", binding.tvStreamUrl.getText().toString()));
|
||||||
binding.btnCopyStreamKey.setOnClickListener(v -> copyToClipboard("推流码", binding.tvStreamKey.getText().toString()));
|
binding.btnCopyStreamKey.setOnClickListener(v -> copyToClipboard("推流码", binding.tvStreamKey.getText().toString()));
|
||||||
binding.btnGetStreamInfo.setOnClickListener(v -> createPcLiveRoom());
|
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) {
|
private void showBottomTab(int position) {
|
||||||
currentTab = position;
|
currentBottomTab = position;
|
||||||
binding.contentMobileLive.setVisibility(View.GONE);
|
|
||||||
binding.contentDynamic.setVisibility(View.GONE);
|
// 切换tab时隐藏键盘
|
||||||
binding.contentWork.setVisibility(View.GONE);
|
hideKeyboard();
|
||||||
binding.contentPcLive.setVisibility(View.GONE);
|
|
||||||
switch (position) {
|
// 根据tab切换底部导航栏样式
|
||||||
case 0: binding.contentMobileLive.setVisibility(View.VISIBLE); break;
|
if (position == 1 || (position == 2 && isMobileLiveMode)) {
|
||||||
case 1: binding.contentDynamic.setVisibility(View.VISIBLE); break;
|
// 相机模式或手机直播模式:透明背景,白色字体
|
||||||
case 2: binding.contentWork.setVisibility(View.VISIBLE); break;
|
binding.bottomNav.setBackgroundColor(0x00000000); // 透明
|
||||||
case 3: binding.contentPcLive.setVisibility(View.VISIBLE); break;
|
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() {
|
private void loadCategories() {
|
||||||
|
|
@ -203,11 +487,8 @@ public class PublishCenterActivity extends AppCompatActivity {
|
||||||
binding.btnPublishDynamic.setText("发布中...");
|
binding.btnPublishDynamic.setText("发布中...");
|
||||||
|
|
||||||
if (dynamicCoverUri != null) {
|
if (dynamicCoverUri != null) {
|
||||||
// 用户选择了封面,上传后发布
|
|
||||||
uploadDynamicCoverAndPublish(content);
|
uploadDynamicCoverAndPublish(content);
|
||||||
} else {
|
} else {
|
||||||
// 用户没选封面,使用特殊标记(纯文字动态)
|
|
||||||
// 使用 "text_only" 作为标记,在展示时识别为纯文字动态
|
|
||||||
doPublishDynamic(content, "text_only");
|
doPublishDynamic(content, "text_only");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -253,7 +534,6 @@ public class PublishCenterActivity extends AppCompatActivity {
|
||||||
request.setDescription(content);
|
request.setDescription(content);
|
||||||
request.setType("IMAGE");
|
request.setType("IMAGE");
|
||||||
request.setImageUrls(new ArrayList<>());
|
request.setImageUrls(new ArrayList<>());
|
||||||
// 如果没有封面,设置特殊标识URL,表示这是纯文字动态
|
|
||||||
if (coverUrl == null || coverUrl.isEmpty()) {
|
if (coverUrl == null || coverUrl.isEmpty()) {
|
||||||
request.setCoverUrl("TEXT_ONLY_DYNAMIC_PLACEHOLDER");
|
request.setCoverUrl("TEXT_ONLY_DYNAMIC_PLACEHOLDER");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -283,7 +563,7 @@ public class PublishCenterActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private void resetDynamicButton() {
|
private void resetDynamicButton() {
|
||||||
binding.btnPublishDynamic.setEnabled(true);
|
binding.btnPublishDynamic.setEnabled(true);
|
||||||
binding.btnPublishDynamic.setText("发布动态");
|
binding.btnPublishDynamic.setText("发布");
|
||||||
}
|
}
|
||||||
|
|
||||||
private File uriToFile(Uri uri) {
|
private File uriToFile(Uri uri) {
|
||||||
|
|
@ -317,10 +597,9 @@ public class PublishCenterActivity extends AppCompatActivity {
|
||||||
binding.btnGetStreamInfo.setText("创建中...");
|
binding.btnGetStreamInfo.setText("创建中...");
|
||||||
binding.streamInfoLoading.setVisibility(View.VISIBLE);
|
binding.streamInfoLoading.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
// 获取当前用户昵称作为主播名称
|
|
||||||
String streamerName = com.example.livestreaming.net.AuthStore.getNickname(this);
|
String streamerName = com.example.livestreaming.net.AuthStore.getNickname(this);
|
||||||
if (TextUtils.isEmpty(streamerName)) {
|
if (TextUtils.isEmpty(streamerName)) {
|
||||||
streamerName = title; // 如果没有昵称,使用标题
|
streamerName = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateRoomRequest request = new CreateRoomRequest(title, streamerName, "pc");
|
CreateRoomRequest request = new CreateRoomRequest(title, streamerName, "pc");
|
||||||
|
|
@ -375,4 +654,282 @@ public class PublishCenterActivity extends AppCompatActivity {
|
||||||
Toast.makeText(this, label + "已复制", Toast.LENGTH_SHORT).show();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,6 +1,21 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:shape="rectangle">
|
<item android:state_pressed="true">
|
||||||
<solid android:color="#FF4757" />
|
<shape android:shape="rectangle">
|
||||||
<corners android:radius="8dp" />
|
<solid android:color="#E53E4F" />
|
||||||
</shape>
|
<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>
|
||||||
|
|
|
||||||
6
android-app/app/src/main/res/drawable/bg_record_time.xml
Normal file
6
android-app/app/src/main/res/drawable/bg_record_time.xml
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
13
android-app/app/src/main/res/drawable/ic_flip_camera_24.xml
Normal file
13
android-app/app/src/main/res/drawable/ic_flip_camera_24.xml
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
276
android-app/app/src/main/res/layout/activity_camera_capture.xml
Normal file
276
android-app/app/src/main/res/layout/activity_camera_capture.xml
Normal 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>
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user