一个基础的一体化播放控制器,用于提升用户体验,减少界面切换带来的不便,以及提高开发效率和性能。
依赖添加
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation 'androidx.cardview:cardview:1.0.0'
项目已上传至github,musicplayercontrol模块
https://github.com/yyoujinga/smalldemos
播放器首页一般为歌曲list和底部控制器,点击底部控制器可打开全屏播放控制器,为实现一体化,底部控制器和全屏播放控制器同处一个fragment文件。
因此
1activity中整体xml布局可分为recycleview和装载控制器的控件(fragmentcontainer),此处我使用cardview实现。
2activity文件中,动态使用bottomsheet实现控制器的状态,折叠状态对应底部播放器,展开对应全屏播放器,设置默认高度为底部播放器高度,并初始化fragment,将音乐数据装载至recycleview。
3fragment中整体xml布局可分为底部控制器和全屏播放控制器,其中全屏默认gone,只显示底部控制器,且底部控制器需在布局的top,否则会出现在首页底部控制器为空白的状况。
4fragment文件中,设置展开、折叠的动画(也可额外添加slide动画,实现滑动效果),控制底部、全屏播放控制器的显示。
5其他,可用spf保存播放器状态,但这方面需求不多,我实现的比较简单。
bottomsheet具体介绍可参考以下博文
https://blog.csdn.net/m0_73986294/article/details/134366315
public class PlayerActivity extends AppCompatActivity {
private PlayerFragment playerFragment;
private PreferencesUtility preferencesUtility;
private BottomSheetBehavior<View> behavior;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
preferencesUtility = PreferencesUtility.getInstance(this);
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
recyclerView.setAdapter(new MusicAdapter(MusicInfo.getTestData()));
// 初始化 Fragment
if (savedInstanceState == null) {
playerFragment = new PlayerFragment();
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, playerFragment)
.commit();
}
// 设置 BottomSheet
View bottomSheet = findViewById(R.id.fragment_container);
behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setPeekHeight(getResources().getDimensionPixelSize(R.dimen.player_min_height));
// 设置点击事件来展开 BottomSheet
bottomSheet.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 只有在当前状态是 COLLAPSED 时才展开
if (behavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
});
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged( View view, int newState) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
preferencesUtility.setPlayerState(true);
if (playerFragment != null) {
playerFragment.onExpanded();
}
} else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
preferencesUtility.setPlayerState(false);
if (playerFragment != null) {
playerFragment.onCollapsed();
}
}
}
@Override
public void onSlide( View view, float slideOffset) {
playerFragment.onSlide(slideOffset);
}
});
}
@Override
public void onBackPressed() {
if (behavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
super.onBackPressed();
}
}
}
xml文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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">
<!-- 主内容区域 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="64dp"
android:layout_marginBottom="64dp"/>
<!-- 播放器容器 -->
<androidx.cardview.widget.CardView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
app:behavior_peekHeight="64dp"
app:behavior_hideable="false"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
public class PlayerFragment extends Fragment {
private View rootView;
private View miniPlayer;
private View fullPlayer;
private ImageView albumArtMini;
private ImageView albumArtFull;
private TextView titleMini;
private TextView titleFull;
private ImageButton playPauseButton;
private SeekBar seekBar;
private MediaPlayer mediaPlayer;
private boolean isPlaying = false;
private boolean isPrepared = false;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_player, container, false);
initViews();
initdata();
setupListeners();
return rootView;
}
private void initdata() {
}
private void initViews() {
miniPlayer = rootView.findViewById(R.id.mini_player);
fullPlayer = rootView.findViewById(R.id.full_player);
albumArtMini = rootView.findViewById(R.id.album_art_mini);
albumArtFull = rootView.findViewById(R.id.album_art_full);
titleMini = rootView.findViewById(R.id.title_mini);
titleFull = rootView.findViewById(R.id.title_full);
playPauseButton = rootView.findViewById(R.id.play_pause_button);
seekBar = rootView.findViewById(R.id.seek_bar);
// 初始状态
fullPlayer.setVisibility(View.GONE);
fullPlayer.setAlpha(0);
miniPlayer.setVisibility(View.VISIBLE);
miniPlayer.setAlpha(1);
}
private void setupListeners() {
playPauseButton.setOnClickListener(v -> togglePlayPause());
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser && mediaPlayer != null) {
mediaPlayer.seekTo(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
}
public void onExpanded() {
miniPlayer.animate()
.alpha(0)
.setDuration(200)
.withEndAction(() -> miniPlayer.setVisibility(View.GONE));
fullPlayer.setVisibility(View.VISIBLE);
fullPlayer.animate()
.alpha(1)
.setDuration(200);
}
public void onCollapsed() {
fullPlayer.animate()
.alpha(0)
.setDuration(200)
.withEndAction(() -> fullPlayer.setVisibility(View.GONE));
miniPlayer.setVisibility(View.VISIBLE);
miniPlayer.animate()
.alpha(1)
.setDuration(200);
}
public void onSlide(float slideOffset) {
miniPlayer.setAlpha(1 - slideOffset);
fullPlayer.setAlpha(slideOffset);
if (slideOffset > 0 && fullPlayer.getVisibility() == View.GONE) {
fullPlayer.setVisibility(View.VISIBLE);
}
}
private void togglePlayPause() {
if (mediaPlayer != null) {
if (isPlaying) {
mediaPlayer.pause();
playPauseButton.setImageResource(R.drawable.ic_play);
} else {
mediaPlayer.start();
playPauseButton.setImageResource(R.drawable.ic_pause);
}
isPlaying = !isPlaying;
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
}
}
}
xml文件
adapter和musicinfo文件比较简单
public class PreferencesUtility {
private static PreferencesUtility sInstance;
private final SharedPreferences mPreferences;
private PreferencesUtility(Context context) {
mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
}
public static PreferencesUtility getInstance(Context context) {
if (sInstance == null) {
sInstance = new PreferencesUtility(context.getApplicationContext());
}
return sInstance;
}
public boolean isFirstTime() {
return mPreferences.getBoolean("first_time", true);
}
public void setFirstTime(boolean firstTime) {
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean("first_time", firstTime);
editor.apply();
}
// 播放器状态
public void setPlayerState(boolean isExpanded) {
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean("player_expanded", isExpanded);
editor.apply();
}
public boolean getPlayerState() {
return mPreferences.getBoolean("player_expanded", false);
}
// 播放模式
public void setPlayMode(int mode) {
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putInt("play_mode", mode);
editor.apply();
}
public int getPlayMode() {
return mPreferences.getInt("play_mode", 0);
}
}