A面前台播放实现
This commit is contained in:
parent
0f54dc33fb
commit
0787a71679
@ -8,7 +8,8 @@
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
|
||||
@ -31,12 +32,12 @@
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.activity.PlayActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.activity.HomeActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="true" >
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@ -60,12 +61,20 @@
|
||||
|
||||
<service
|
||||
android:name=".media3.PlaybackService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.MusicPlayerForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@ -0,0 +1,100 @@
|
||||
package com.hi.music.player.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.hi.music.player.R;
|
||||
import com.hi.music.player.javabean.A_data.AudioItem;
|
||||
import com.hi.music.player.ui.activity.A_PlayActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class A_ImportFragmentAdapter extends RecyclerView.Adapter<A_ImportFragmentAdapter.ViewHolder> {
|
||||
|
||||
private Context context;
|
||||
private List<AudioItem> audioFiles = new ArrayList<>();
|
||||
private OnDeleteClickListener onDeleteClickListener;
|
||||
|
||||
public A_ImportFragmentAdapter(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_a_import, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
AudioItem audioItem = audioFiles.get(position);
|
||||
|
||||
holder.title.setText(audioItem.getName());
|
||||
holder.time.setText(audioItem.getDuration());
|
||||
Log.d("Adapter", "onBindViewHolder: " + audioItem.getDuration());
|
||||
|
||||
holder.deleteButton.setOnClickListener(v -> {
|
||||
if (onDeleteClickListener != null) {
|
||||
onDeleteClickListener.onDeleteClick(audioItem.getFile());
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(context, A_PlayActivity.class);
|
||||
intent.putExtra("Path", audioItem);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return audioFiles.size();
|
||||
}
|
||||
|
||||
public void setAudioFiles(List<AudioItem> audioFiles) {
|
||||
this.audioFiles = audioFiles;
|
||||
notifyDataSetChanged(); // Update UI when the data changes
|
||||
}
|
||||
|
||||
public void setOnDeleteClickListener(OnDeleteClickListener listener) {
|
||||
this.onDeleteClickListener = listener;
|
||||
}
|
||||
|
||||
public interface OnDeleteClickListener {
|
||||
void onDeleteClick(String filePath);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
ImageView deleteButton;
|
||||
TextView title;
|
||||
TextView time;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
deleteButton = itemView.findViewById(R.id.options);
|
||||
title = itemView.findViewById(R.id.title);
|
||||
time = itemView.findViewById(R.id.time);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,8 @@ public class CircularProgressBar extends View {
|
||||
private int fixedSize = 47; // 固定圆环的直径(dp)
|
||||
private int progressWidth = 5; // 固定进度条宽度(dp)
|
||||
|
||||
private OnProgressChangeListener listener; // 自定义监听器
|
||||
|
||||
public CircularProgressBar(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
@ -35,7 +37,7 @@ public class CircularProgressBar extends View {
|
||||
// 初始化用于绘制进度条的画笔
|
||||
progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
progressPaint.setStyle(Paint.Style.STROKE);
|
||||
progressPaint.setStrokeWidth(dpToPx(progressWidth)); // 设置进度条宽度(10dp)
|
||||
progressPaint.setStrokeWidth(dpToPx(progressWidth)); // 设置进度条宽度(dp)
|
||||
progressPaint.setColor(Color.WHITE); // 设置进度条颜色为白色
|
||||
|
||||
// 初始化用于绘制背景环的画笔
|
||||
@ -85,6 +87,9 @@ public class CircularProgressBar extends View {
|
||||
|
||||
public void setProgress(float progress) {
|
||||
this.progress = Math.max(0, Math.min(progress, maxProgress)); // 限制进度值在 0 到 maxProgress 之间
|
||||
if (listener != null) {
|
||||
listener.onProgressChanged(this, (int) progress); // 通知监听器进度变化
|
||||
}
|
||||
invalidate(); // 请求重新绘制视图
|
||||
}
|
||||
|
||||
@ -94,6 +99,16 @@ public class CircularProgressBar extends View {
|
||||
invalidate(); // 请求重新绘制视图
|
||||
}
|
||||
|
||||
// 添加设置监听器的方法
|
||||
public void setOnProgressChangeListener(OnProgressChangeListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
// 自定义进度变化监听器接口
|
||||
public interface OnProgressChangeListener {
|
||||
void onProgressChanged(CircularProgressBar progressBar, int progress);
|
||||
}
|
||||
|
||||
// 将 dp 转换为 px
|
||||
private int dpToPx(int dp) {
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
|
||||
@ -6,6 +6,7 @@ public class AudioItem implements Serializable {
|
||||
private String name;
|
||||
private String file;
|
||||
private String image;
|
||||
private String duration;
|
||||
|
||||
public AudioItem(String name, String file, String image) {
|
||||
this.name = name;
|
||||
@ -36,4 +37,12 @@ public class AudioItem implements Serializable {
|
||||
public void setImage(String image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
public String getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(String duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,166 @@
|
||||
package com.hi.music.player.service;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.hi.music.player.R;
|
||||
import com.hi.music.player.ui.activity.A_PlayActivity;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class MusicPlayerForegroundService extends Service {
|
||||
|
||||
public static final String CHANNEL_ID = "MusicPlayerChannel";
|
||||
private MediaPlayer mediaPlayer;
|
||||
private final IBinder binder = new MusicBinder();
|
||||
private MutableLiveData<Boolean> isPlaying = new MutableLiveData<>(false);
|
||||
|
||||
public class MusicBinder extends Binder {
|
||||
public MusicPlayerForegroundService getService() {
|
||||
return MusicPlayerForegroundService.this;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
String audioPath = intent.getStringExtra("Path");
|
||||
if (audioPath != null) {
|
||||
initializePlayer(audioPath);
|
||||
}
|
||||
|
||||
startForeground(1, createNotification("Playing Audio", "Your audio is playing"));
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
private Notification createNotification(String title, String content) {
|
||||
createNotificationChannel();
|
||||
Intent notificationIntent = new Intent(this, A_PlayActivity.class);
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
}
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags);
|
||||
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setSmallIcon(R.drawable.home_select)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel serviceChannel = new NotificationChannel(
|
||||
CHANNEL_ID, "Music Player Channel", NotificationManager.IMPORTANCE_LOW);
|
||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
||||
manager.createNotificationChannel(serviceChannel);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePlayer(String path) {
|
||||
try {
|
||||
Log.d("MediaPlayerError","path: "+path);
|
||||
|
||||
if (mediaPlayer == null) {
|
||||
mediaPlayer = new MediaPlayer();
|
||||
} else {
|
||||
mediaPlayer.reset();
|
||||
}
|
||||
|
||||
if (path.startsWith("content://")) {
|
||||
Uri contentUri = Uri.parse(path);
|
||||
ContentResolver contentResolver = getContentResolver();
|
||||
AssetFileDescriptor afd = contentResolver.openAssetFileDescriptor(contentUri, "r");
|
||||
if (afd != null) {
|
||||
mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
|
||||
afd.close();
|
||||
}
|
||||
} else {
|
||||
AssetFileDescriptor afd = getAssets().openFd(path);
|
||||
mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
|
||||
afd.close();
|
||||
}
|
||||
|
||||
mediaPlayer.prepare();
|
||||
mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start());
|
||||
isPlaying.postValue(true);
|
||||
} catch (IOException e) {
|
||||
Log.e("MediaPlayerError", "Could not open file: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseAudio() {
|
||||
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
|
||||
mediaPlayer.pause();
|
||||
isPlaying.postValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void resumeAudio() {
|
||||
if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
|
||||
mediaPlayer.start();
|
||||
isPlaying.postValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
public MutableLiveData<Boolean> getIsPlaying() {
|
||||
return isPlaying;
|
||||
}
|
||||
|
||||
// 添加获取当前播放位置的方法
|
||||
public int getCurrentPosition() {
|
||||
if (mediaPlayer != null) {
|
||||
return mediaPlayer.getCurrentPosition();
|
||||
}
|
||||
return 0; // 如果 mediaPlayer 为 null,返回 0
|
||||
}
|
||||
|
||||
// 添加获取总时长的方法
|
||||
public int getDuration() {
|
||||
if (mediaPlayer != null) {
|
||||
return mediaPlayer.getDuration();
|
||||
}
|
||||
return 0; // 如果 mediaPlayer 为 null,返回 0
|
||||
}
|
||||
|
||||
public void seekTo(int position) {
|
||||
if (mediaPlayer != null) {
|
||||
mediaPlayer.seekTo(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mediaPlayer != null) {
|
||||
mediaPlayer.release();
|
||||
mediaPlayer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,21 +1,24 @@
|
||||
package com.hi.music.player.ui.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
import com.hi.music.player.R;
|
||||
import com.hi.music.player.adapter.A_HomeViewPagerAdapter;
|
||||
import com.hi.music.player.databinding.ActivityAhomeBinding;
|
||||
import com.hi.music.player.databinding.HomeTabCustomBinding;
|
||||
import com.hi.music.player.helper.CircularProgressBar;
|
||||
import com.hi.music.player.ui.activity.viewmodel.A_VMPlay;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class A_HomeActivity extends BaseActivity<ActivityAhomeBinding> {
|
||||
|
||||
// 图标数组定义为类成员,避免重复
|
||||
private final int[] defaultIcons = {
|
||||
R.drawable.home_unselect,
|
||||
R.drawable.import_unselect,
|
||||
@ -26,11 +29,7 @@ public class A_HomeActivity extends BaseActivity<ActivityAhomeBinding> {
|
||||
R.drawable.import_select,
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
}
|
||||
private A_VMPlay viewModel;
|
||||
|
||||
@Override
|
||||
protected ActivityAhomeBinding getViewBinding() {
|
||||
@ -39,9 +38,9 @@ public class A_HomeActivity extends BaseActivity<ActivityAhomeBinding> {
|
||||
|
||||
@Override
|
||||
protected void onCreateInit() {
|
||||
|
||||
viewModel = new ViewModelProvider(this).get(A_VMPlay.class);
|
||||
initData();
|
||||
|
||||
setupObservers();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -60,41 +59,72 @@ public class A_HomeActivity extends BaseActivity<ActivityAhomeBinding> {
|
||||
}
|
||||
|
||||
public void initData() {
|
||||
|
||||
A_HomeViewPagerAdapter adapter = new A_HomeViewPagerAdapter(this);
|
||||
vb.homeViewpager.setAdapter(adapter);
|
||||
vb.homeViewpager.setUserInputEnabled(false);
|
||||
|
||||
// 设置TabLayout的图标
|
||||
new TabLayoutMediator(vb.homeTabLayout, vb.homeViewpager, (tab, position) -> {
|
||||
HomeTabCustomBinding tabBinding = HomeTabCustomBinding.inflate(LayoutInflater.from(this));
|
||||
tab.setCustomView(tabBinding.getRoot());
|
||||
tabBinding.homeIcon.setImageResource(defaultIcons[position]); // 默认图标
|
||||
tabBinding.homeIcon.setImageResource(defaultIcons[position]);
|
||||
}).attach();
|
||||
|
||||
// 添加Tab选中与未选中事件监听器
|
||||
vb.homeTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
updateTabIcon(tab, true); // 更新选中的图标
|
||||
updateTabIcon(tab, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
updateTabIcon(tab, false); // 恢复未选中图标
|
||||
updateTabIcon(tab, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
// 可选:重复选择Tab时的操作
|
||||
}
|
||||
});
|
||||
|
||||
// 设置默认选中第一个
|
||||
TabLayout.Tab firstTab = vb.homeTabLayout.getTabAt(0);
|
||||
if (firstTab != null) {
|
||||
firstTab.select();
|
||||
updateTabIcon(firstTab, true); // 设置选中的图标
|
||||
updateTabIcon(firstTab, true);
|
||||
}
|
||||
|
||||
vb.circularProgressBar.setMaxProgress(100); // 假设最大值为100
|
||||
|
||||
vb.circularProgressBar.setOnProgressChangeListener(new CircularProgressBar.OnProgressChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(CircularProgressBar progressBar, int progress) {
|
||||
viewModel.seekTo(progress); // 当用户改变进度时,调用 ViewModel 方法
|
||||
}
|
||||
});
|
||||
|
||||
vb.pause.setOnClickListener(v -> {
|
||||
viewModel.togglePlay();
|
||||
});
|
||||
}
|
||||
|
||||
private void setupObservers() {
|
||||
viewModel.getCurrentTime().observe(this, currentTime -> {
|
||||
vb.bottomText.setText(currentTime);
|
||||
});
|
||||
|
||||
viewModel.getTotalTime().observe(this, totalTime -> {
|
||||
vb.topText.setText(totalTime);
|
||||
});
|
||||
|
||||
viewModel.getProgress().observe(this, progress -> {
|
||||
if (progress >= 0 && progress <= 100) {
|
||||
vb.circularProgressBar.setProgress(progress); // 更新进度条
|
||||
} else {
|
||||
Log.e("A_HomeActivity", "Progress out of range: " + progress);
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.isPlaying().observe(this, isPlaying -> {
|
||||
vb.pause.setImageResource(isPlaying ? R.drawable.play : R.drawable.pause);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateTabIcon(TabLayout.Tab tab, boolean isSelected) {
|
||||
@ -108,4 +138,3 @@ public class A_HomeActivity extends BaseActivity<ActivityAhomeBinding> {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,23 +1,46 @@
|
||||
package com.hi.music.player.ui.activity;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.MediaPlayer;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.hi.music.player.R;
|
||||
import com.hi.music.player.databinding.ActivityAplayBinding;
|
||||
import com.hi.music.player.javabean.A_data.AudioItem;
|
||||
import com.hi.music.player.ui.activity.viewmodel.A_VMPlay;
|
||||
import com.hi.music.player.service.MusicPlayerForegroundService;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class A_PlayActivity extends BaseActivity<ActivityAplayBinding> {
|
||||
|
||||
private AudioItem audioItem;
|
||||
private MediaPlayer mediaPlayer;
|
||||
private A_VMPlay playViewModel;
|
||||
private MusicPlayerForegroundService musicService;
|
||||
private boolean isBound = false;
|
||||
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
MusicPlayerForegroundService.MusicBinder binder = (MusicPlayerForegroundService.MusicBinder) service;
|
||||
musicService = binder.getService();
|
||||
isBound = true;
|
||||
|
||||
// 观察服务中的播放状态
|
||||
musicService.getIsPlaying().observe(A_PlayActivity.this, isPlaying -> {
|
||||
vb.playButton.setBackgroundResource(isPlaying ? R.drawable.play : R.drawable.pause);
|
||||
});
|
||||
|
||||
// 更新进度和总时长
|
||||
startUpdatingProgress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
isBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected ActivityAplayBinding getViewBinding() {
|
||||
@ -26,68 +49,74 @@ public class A_PlayActivity extends BaseActivity<ActivityAplayBinding> {
|
||||
|
||||
@Override
|
||||
protected void onCreateInit() {
|
||||
|
||||
Intent intent = getIntent();
|
||||
audioItem = (AudioItem) intent.getSerializableExtra("Path");
|
||||
AudioItem audioItem = (AudioItem) intent.getSerializableExtra("Path");
|
||||
if (audioItem == null) {
|
||||
finish(); // 如果没有音频项目,结束活动
|
||||
return;
|
||||
}
|
||||
|
||||
String path = audioItem.getFile();
|
||||
Log.d("------", "--------" + path);
|
||||
// 启动前台服务
|
||||
Intent serviceIntent = new Intent(this, MusicPlayerForegroundService.class);
|
||||
serviceIntent.putExtra("Path", audioItem.getFile());
|
||||
Log.d("A_PlayActivity", "Path: " + audioItem.getFile());
|
||||
startService(serviceIntent);
|
||||
bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
playViewModel = new ViewModelProvider(this).get(A_VMPlay.class);
|
||||
mediaPlayer = new MediaPlayer();
|
||||
|
||||
playViewModel.initializePlayer(path);
|
||||
|
||||
// 观察进度更新
|
||||
playViewModel.getProgress().observe(this, progress -> {
|
||||
vb.songSeekbar.setProgress(progress);
|
||||
});
|
||||
|
||||
// 观察播放状态
|
||||
playViewModel.isPlaying().observe(this, isPlaying -> {
|
||||
vb.playButton.setBackgroundResource(isPlaying ? R.drawable.play : R.drawable.pause);
|
||||
});
|
||||
|
||||
// 观察当前时间
|
||||
playViewModel.getCurrentTime().observe(this, currentTime -> {
|
||||
vb.current.setText(currentTime);
|
||||
});
|
||||
|
||||
// 观察总时间
|
||||
playViewModel.getTotalTime().observe(this, totalTime -> {
|
||||
vb.time.setText(totalTime);
|
||||
vb.playButton.setOnClickListener(v -> {
|
||||
if (isBound) {
|
||||
if (musicService.getIsPlaying().getValue() != null && musicService.getIsPlaying().getValue()) {
|
||||
musicService.pauseAudio();
|
||||
} else {
|
||||
musicService.resumeAudio();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 启动定期更新播放进度
|
||||
startUpdatingProgress();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInitClick() {
|
||||
vb.playButton.setOnClickListener(v -> playViewModel.togglePlay());
|
||||
|
||||
vb.songSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
playViewModel.seekTo(progress); // 拖动进度条时控制音频
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
// 暂停更新进度
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
// 恢复更新进度
|
||||
}
|
||||
private void startUpdatingProgress() {
|
||||
new Thread(() -> {
|
||||
while (isBound && musicService != null) {
|
||||
if (musicService.getIsPlaying().getValue() != null && musicService.getIsPlaying().getValue()) {
|
||||
int currentPosition = musicService.getCurrentPosition(); // 获取当前播放位置
|
||||
int duration = musicService.getDuration(); // 获取总时长
|
||||
runOnUiThread(() -> {
|
||||
vb.songSeekbar.setProgress((int) ((currentPosition / (float) duration) * 100)); // 更新进度条
|
||||
vb.current.setText(formatTime(currentPosition)); // 更新当前时间文本
|
||||
vb.time.setText(formatTime(duration)); // 更新总时间文本
|
||||
});
|
||||
|
||||
}
|
||||
try {
|
||||
Thread.sleep(1000); // 每秒更新一次
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private String formatTime(int milliseconds) {
|
||||
int minutes = (milliseconds / 1000) / 60;
|
||||
int seconds = (milliseconds / 1000) % 60;
|
||||
return String.format(Locale.getDefault(), "%d:%02d", minutes, seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (isBound) {
|
||||
unbindService(serviceConnection);
|
||||
isBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullScreen() {
|
||||
|
||||
@ -1,26 +1,33 @@
|
||||
package com.hi.music.player.ui.activity.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.media.MediaPlayer;
|
||||
import android.util.Log;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.io.IOException;
|
||||
import com.hi.music.player.service.MusicPlayerForegroundService;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class A_VMPlay extends AndroidViewModel {
|
||||
private MediaPlayer mediaPlayer;
|
||||
private final MutableLiveData<String> currentTime = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> totalTime = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> fileName = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> isPlaying = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Integer> progress = new MutableLiveData<>(0); // 添加进度 LiveData
|
||||
|
||||
private MusicPlayerForegroundService musicService;
|
||||
private boolean isBound = false;
|
||||
|
||||
public A_VMPlay(Application application) {
|
||||
super(application);
|
||||
mediaPlayer = new MediaPlayer();
|
||||
bindToMusicService();
|
||||
}
|
||||
|
||||
// 获取进度的 LiveData
|
||||
@ -40,37 +47,54 @@ public class A_VMPlay extends AndroidViewModel {
|
||||
return isPlaying;
|
||||
}
|
||||
|
||||
public void initializePlayer(String path) {
|
||||
try {
|
||||
AssetFileDescriptor afd = getApplication().getAssets().openFd(path);
|
||||
mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
|
||||
mediaPlayer.prepare();
|
||||
mediaPlayer.setOnPreparedListener(mp -> {
|
||||
totalTime.setValue(formatTime(mediaPlayer.getDuration()));
|
||||
startUpdatingProgress();
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Log.e("MediaPlayerError", "Could not open asset file: " + path, e);
|
||||
}
|
||||
public LiveData<String> getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
private void bindToMusicService() {
|
||||
Intent serviceIntent = new Intent(getApplication(), MusicPlayerForegroundService.class);
|
||||
getApplication().bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
MusicPlayerForegroundService.MusicBinder binder = (MusicPlayerForegroundService.MusicBinder) service;
|
||||
musicService = binder.getService();
|
||||
isBound = true;
|
||||
|
||||
// 观察服务中的播放状态
|
||||
musicService.getIsPlaying().observeForever(isPlaying::setValue);
|
||||
|
||||
// 开始监听播放进度
|
||||
startUpdatingProgress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
isBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
public void togglePlay() {
|
||||
if (mediaPlayer.isPlaying()) {
|
||||
mediaPlayer.pause();
|
||||
isPlaying.setValue(false);
|
||||
if (isBound) {
|
||||
if (musicService.getIsPlaying().getValue() != null && musicService.getIsPlaying().getValue()) {
|
||||
musicService.pauseAudio();
|
||||
} else {
|
||||
mediaPlayer.start();
|
||||
isPlaying.setValue(true);
|
||||
musicService.resumeAudio();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startUpdatingProgress() {
|
||||
new Thread(() -> {
|
||||
while (mediaPlayer != null) {
|
||||
if (mediaPlayer.isPlaying()) {
|
||||
int currentPosition = mediaPlayer.getCurrentPosition();
|
||||
while (isBound && musicService != null) {
|
||||
if (musicService.getIsPlaying().getValue() != null && musicService.getIsPlaying().getValue()) {
|
||||
int currentPosition = musicService.getCurrentPosition(); // 从服务获取当前播放位置
|
||||
currentTime.postValue(formatTime(currentPosition));
|
||||
progress.postValue((int) ((currentPosition / (float) mediaPlayer.getDuration()) * 100)); // 更新进度
|
||||
int duration = musicService.getDuration(); // 从服务获取音频总时长
|
||||
progress.postValue((int) ((currentPosition / (float) duration) * 100)); // 更新进度
|
||||
totalTime.postValue(formatTime(duration));
|
||||
}
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
@ -82,24 +106,25 @@ public class A_VMPlay extends AndroidViewModel {
|
||||
}
|
||||
|
||||
public void seekTo(int progress) {
|
||||
if (mediaPlayer != null) {
|
||||
int duration = mediaPlayer.getDuration();
|
||||
mediaPlayer.seekTo((int) (duration * progress / 100.0));
|
||||
if (isBound && musicService != null) {
|
||||
int duration = musicService.getDuration();
|
||||
musicService.seekTo((int) (duration * progress / 100.0));
|
||||
}
|
||||
}
|
||||
|
||||
private String formatTime(int milliseconds) {
|
||||
int minutes = (milliseconds / 1000) / 60;
|
||||
int seconds = (milliseconds / 1000) % 60;
|
||||
return String.format("%02d:%02d", minutes, seconds);
|
||||
return String.format(Locale.getDefault(), "%d:%02d", minutes, seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
if (mediaPlayer != null) {
|
||||
mediaPlayer.release();
|
||||
mediaPlayer = null;
|
||||
if (isBound) {
|
||||
getApplication().unbindService(serviceConnection);
|
||||
isBound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,19 +7,30 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.hi.music.player.adapter.A_ImportFragmentAdapter;
|
||||
import com.hi.music.player.databinding.FragmentAImportBinding;
|
||||
import com.hi.music.player.javabean.A_data.AudioItem;
|
||||
import com.hi.music.player.ui.activity.A_SettingActivity;
|
||||
import com.hi.music.player.ui.fragmnt.viewmodel.A_VMImport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class A_ImportFragment extends BaseFragment<FragmentAImportBinding> {
|
||||
|
||||
private static final int REQUEST_CODE_READ_MEDIA_AUDIO = 1001;
|
||||
private static final int REQUEST_CODE_PICK_AUDIO = 1002;
|
||||
private A_ImportFragmentAdapter adapter;
|
||||
private A_VMImport viewModel;
|
||||
|
||||
@Override
|
||||
protected FragmentAImportBinding getFragmentVb() {
|
||||
@ -28,47 +39,59 @@ public class A_ImportFragment extends BaseFragment<FragmentAImportBinding> {
|
||||
|
||||
@Override
|
||||
protected void initView() {
|
||||
viewModel = new ViewModelProvider(this).get(A_VMImport.class);
|
||||
adapter = new A_ImportFragmentAdapter(requireContext());
|
||||
|
||||
initData();
|
||||
Vb.importRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
Vb.importRecycler.setAdapter(adapter);
|
||||
|
||||
// 监听音频文件列表变化
|
||||
viewModel.getAudioFiles().observe(getViewLifecycleOwner(), this::updateAudioList);
|
||||
|
||||
initEvent();
|
||||
|
||||
}
|
||||
|
||||
public void initData() {
|
||||
|
||||
// 更新音频文件列表
|
||||
private void updateAudioList(List<AudioItem> audioFiles) {
|
||||
adapter.setAudioFiles(audioFiles);
|
||||
}
|
||||
|
||||
// 初始化事件
|
||||
public void initEvent() {
|
||||
|
||||
Vb.setting.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(getContext(), A_SettingActivity.class);
|
||||
startActivity(intent);
|
||||
});
|
||||
|
||||
Vb.add.setOnClickListener(v -> {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_MEDIA_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(requireActivity(),
|
||||
new String[]{Manifest.permission.READ_MEDIA_AUDIO},
|
||||
REQUEST_CODE_READ_MEDIA_AUDIO);
|
||||
} else {
|
||||
openAudioPicker();
|
||||
}
|
||||
} else {
|
||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(requireActivity(),
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
REQUEST_CODE_READ_MEDIA_AUDIO);
|
||||
} else {
|
||||
openAudioPicker();
|
||||
}
|
||||
}
|
||||
|
||||
// 添加按钮的点击事件,打开文件选择器
|
||||
Vb.add.setOnClickListener(v -> checkAndOpenAudioPicker());
|
||||
|
||||
// 适配器中的删除按钮点击事件
|
||||
adapter.setOnDeleteClickListener(filePath -> {
|
||||
viewModel.markAudioAsDeleted(filePath);
|
||||
Toast.makeText(getContext(), "Audio marked as deleted", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
// 检查并请求权限,然后打开音频选择器
|
||||
private void checkAndOpenAudioPicker() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_MEDIA_AUDIO)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(requireActivity(),
|
||||
new String[]{Manifest.permission.READ_MEDIA_AUDIO}, REQUEST_CODE_READ_MEDIA_AUDIO);
|
||||
} else {
|
||||
openAudioPicker(); // 权限已授予,打开选择器
|
||||
}
|
||||
} else {
|
||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(requireActivity(),
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_CODE_READ_MEDIA_AUDIO);
|
||||
} else {
|
||||
openAudioPicker(); // 权限已授予,打开选择器
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理权限请求结果
|
||||
@ -77,6 +100,7 @@ public class A_ImportFragment extends BaseFragment<FragmentAImportBinding> {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_CODE_READ_MEDIA_AUDIO) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// 权限已授予,打开音频选择器
|
||||
openAudioPicker();
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Permission denied", Toast.LENGTH_SHORT).show();
|
||||
@ -86,26 +110,48 @@ public class A_ImportFragment extends BaseFragment<FragmentAImportBinding> {
|
||||
|
||||
// 打开音频选择器
|
||||
private void openAudioPicker() {
|
||||
// 检查外部存储状态
|
||||
String state = Environment.getExternalStorageState();
|
||||
if (!Environment.MEDIA_MOUNTED.equals(state)) {
|
||||
Toast.makeText(getContext(), "外部存储不可用", Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); // 持久 URI 权限
|
||||
intent.setType("audio/*"); // 设置 MIME 类型为音频
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE); // 限制为可打开的文件
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 授予 URI 读取权限
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK_AUDIO);
|
||||
}
|
||||
|
||||
|
||||
// 处理 Activity 结果
|
||||
// 处理音频选择器结果
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_CODE_PICK_AUDIO && resultCode == RESULT_OK) {
|
||||
if (data != null) {
|
||||
Uri audioUri = data.getData();
|
||||
// 处理音频文件,例如播放或显示在列表中
|
||||
Toast.makeText(requireContext(), "Audio Selected: " + audioUri.toString(), Toast.LENGTH_SHORT).show();
|
||||
Uri selectedAudioUri = data.getData();
|
||||
if (selectedAudioUri != null) {
|
||||
// 授予 URI 读取权限
|
||||
requireActivity().grantUriPermission(requireActivity().getPackageName(), selectedAudioUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
// 将音频添加到 ViewModel 中
|
||||
try {
|
||||
viewModel.addAudioFile(selectedAudioUri);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 刷新音频列表
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// 重新获取音频文件列表并更新 UI
|
||||
viewModel.getAudioFiles().observe(getViewLifecycleOwner(), this::updateAudioList);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,127 @@
|
||||
package com.hi.music.player.ui.fragmnt.viewmodel;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import android.app.Application;
|
||||
import android.database.Cursor;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
public class A_VMImport extends ViewModel {
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.hi.music.player.javabean.A_data.AudioItem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class A_VMImport extends AndroidViewModel {
|
||||
|
||||
private static final String PREFS_NAME = "audio_files_prefs";
|
||||
private static final String AUDIO_FILES_KEY = "audio_files";
|
||||
|
||||
private final MutableLiveData<List<AudioItem>> audioFilesLiveData = new MutableLiveData<>();
|
||||
private final SharedPreferences sharedPreferences;
|
||||
private final Gson gson;
|
||||
private final List<AudioItem> audioFiles;
|
||||
|
||||
public A_VMImport(Application application) {
|
||||
super(application);
|
||||
sharedPreferences = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
gson = new Gson();
|
||||
audioFiles = loadAudioFiles(); // 加载已保存的音频文件列表
|
||||
audioFilesLiveData.setValue(new ArrayList<>(audioFiles)); // 设置 LiveData 的初始值
|
||||
}
|
||||
|
||||
// 添加选中的音频文件
|
||||
public void addAudioFile(Uri audioUri) throws IOException {
|
||||
String fileName = null;
|
||||
long duration = 0;
|
||||
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
try {
|
||||
retriever.setDataSource(getApplication(), audioUri);
|
||||
fileName = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
|
||||
String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
||||
if (durationStr != null) {
|
||||
duration = Long.parseLong(durationStr);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("ViewModel", "Failed to retrieve audio metadata", e);
|
||||
} finally {
|
||||
retriever.release(); // 释放资源
|
||||
}
|
||||
|
||||
// 如果 fileName 为空,使用 URI 的最后一部分作为文件名
|
||||
if (fileName == null || fileName.isEmpty()) {
|
||||
fileName = audioUri.getLastPathSegment();
|
||||
}
|
||||
|
||||
if (duration > 0) {
|
||||
AudioItem newItem = new AudioItem(fileName, audioUri.toString(), "");
|
||||
newItem.setDuration(formatDuration(duration)); // 格式化时长
|
||||
|
||||
// 防止重复添加
|
||||
if (!audioFiles.contains(newItem)) {
|
||||
audioFiles.add(newItem);
|
||||
saveAudioFiles(); // 每次添加后保存到 SharedPreferences
|
||||
audioFilesLiveData.setValue(new ArrayList<>(audioFiles)); // 更新 LiveData
|
||||
} else {
|
||||
Toast.makeText(getApplication(), "Audio file already exists in the list.", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(getApplication(), "Failed to retrieve audio duration.", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
// 保存音频文件列表到 SharedPreferences
|
||||
private void saveAudioFiles() {
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
String json = gson.toJson(audioFiles);
|
||||
editor.putString(AUDIO_FILES_KEY, json);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
// 从 SharedPreferences 加载音频文件列表
|
||||
private List<AudioItem> loadAudioFiles() {
|
||||
String json = sharedPreferences.getString(AUDIO_FILES_KEY, null);
|
||||
Type type = new TypeToken<List<AudioItem>>() {}.getType();
|
||||
List<AudioItem> loadedFiles = gson.fromJson(json, type);
|
||||
return loadedFiles != null ? loadedFiles : new ArrayList<>(); // 返回加载的文件列表或空列表
|
||||
}
|
||||
|
||||
// 标记音频文件为已删除
|
||||
public void markAudioAsDeleted(String filePath) {
|
||||
// 从列表中移除该音频
|
||||
for (AudioItem item : new ArrayList<>(audioFiles)) {
|
||||
if (item.getFile().equals(filePath)) {
|
||||
audioFiles.remove(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 LiveData
|
||||
audioFilesLiveData.setValue(new ArrayList<>(audioFiles)); // 更新 LiveData
|
||||
saveAudioFiles(); // 更新 SharedPreferences
|
||||
}
|
||||
|
||||
// 获取音频文件列表
|
||||
public MutableLiveData<List<AudioItem>> getAudioFiles() {
|
||||
return audioFilesLiveData;
|
||||
}
|
||||
|
||||
// 格式化时长
|
||||
private String formatDuration(long duration) {
|
||||
long minutes = (duration / 1000) / 60;
|
||||
long seconds = (duration / 1000) % 60;
|
||||
return String.format(Locale.getDefault(), "%d:%02d", minutes, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
9
app/src/main/res/drawable/options.xml
Normal file
9
app/src/main/res/drawable/options.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="12dp"
|
||||
android:height="12dp"
|
||||
android:viewportWidth="12"
|
||||
android:viewportHeight="12">
|
||||
<path
|
||||
android:pathData="M9.6,6C9.6,5.337 10.137,4.8 10.8,4.8C11.463,4.8 12,5.337 12,6C12,6.663 11.463,7.2 10.8,7.2C10.137,7.2 9.6,6.663 9.6,6ZM4.8,6C4.8,5.337 5.337,4.8 6,4.8C6.663,4.8 7.2,5.337 7.2,6C7.2,6.663 6.663,7.2 6,7.2C5.337,7.2 4.8,6.663 4.8,6ZM0,6C0,5.337 0.537,4.8 1.2,4.8C1.863,4.8 2.4,5.337 2.4,6C2.4,6.663 1.863,7.2 1.2,7.2C0.537,7.2 0,6.663 0,6Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@ -33,7 +33,6 @@
|
||||
android:layout_marginBottom="-5dp"
|
||||
android:background="@drawable/round_rectangle"
|
||||
android:backgroundTint="#80F988"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/home_tab_layout">
|
||||
|
||||
<!-- 包裹 ProgressBar 和 ImageView 的 FrameLayout -->
|
||||
@ -60,7 +59,7 @@
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@mipmap/cover" />
|
||||
android:src="@mipmap/default_image" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- 中间的两排 TextView -->
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
android:layout_height="300dp"
|
||||
android:layout_marginTop="66dp"
|
||||
android:contentDescription="Record"
|
||||
android:src="@mipmap/cover"
|
||||
android:src="@mipmap/default_image"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/now_playing_text" />
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
android:layout_marginTop="32sp"
|
||||
android:text="Parents voice"
|
||||
android:textSize="32sp"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
@ -30,7 +31,7 @@
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/setting_recycler"
|
||||
android:id="@+id/import_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title" />
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@mipmap/cover" />
|
||||
android:src="@mipmap/default_image" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/overlay"
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@mipmap/cover" />
|
||||
android:src="@mipmap/default_image" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@mipmap/cover" />
|
||||
android:src="@mipmap/default_image" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
||||
65
app/src/main/res/layout/item_a_import.xml
Normal file
65
app/src/main/res/layout/item_a_import.xml
Normal file
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card_view"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
app:cardCornerRadius="10dp"
|
||||
app:cardElevation="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cover"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@color/black"
|
||||
android:src="@mipmap/default_image" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="11dp"
|
||||
android:text="Top Text"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toEndOf="@+id/card_view"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="11dp"
|
||||
android:text="Bottom Text"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/card_view"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/options"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:src="@drawable/options"
|
||||
android:padding="5dp"
|
||||
android:background="@drawable/rounded"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
BIN
app/src/main/res/mipmap-xxxhdpi/default_image.png
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/default_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Loading…
Reference in New Issue
Block a user