A面前台播放实现

This commit is contained in:
lihongwei 2024-09-26 18:34:20 +08:00
parent 0f54dc33fb
commit 0787a71679
19 changed files with 782 additions and 162 deletions

View File

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

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

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

View File

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

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB