(1)任务描述
中控大屏仪表屏幕中间显示媒体组件,组件内上方显示正在播放的媒体信息例如“歌曲名称”“音视频名称”信息栏、“音量显示”。
信息栏下方显示播放控制按钮,“播放/暂停”“上一曲下一曲”“音量滑块”“静音”,在操控设备中点击“播放/暂停”,仪表屏中的按钮随之切换“播放/暂停”状态,点击操控设备中的“上一曲下一曲”按钮,仪表屏可切换正在播放媒体信息,点击操控设备中的“静音”按钮,仪表屏幕“音量显示”调节到静音模式。
中控大屏主屏中显示当前播放的歌曲名称、歌曲进度和歌词,显示“播放/暂停”“上一曲下一曲”“音量滑块”“静音”按钮,点击各自按钮可对当前播放的歌曲进行操作。
主页面用卡片列表展示该设备中所有的视频信息,卡片上半部分展示该视频的预览图,下半部分显示视频名称和“上次看到 xx 分xx 秒”信息。
点击对应的视频卡片,对应的屏幕会进入视频播放页面,并播放所选择卡片对应的视频。当点击正在播放的视频时,左上角显示【返回】按钮,点击【返回】则主屏回到影视娱乐 App 主页面。视频播放页面下方显示视频播放器工具栏,工具栏上半部分显示【快进】【快退】【暂停/继续播放】【其他视频】按钮,点击【其他视频】按钮可弹出视频列表弹层,以列表的形式展示其他视频,点击列表项可切换至对应的视频进行播放,页面播放工具栏下半部分显示视频的【当前播放时长】【总时长】和【视频进度条】,可拖动视频进度条调整当前视频播放进度。
步骤一:
首先创建一个android 项目名字自己想
步骤二:在build.gradle中给上ViewBinding等待Sync Now 构建就完成了
步骤三: AndroidManifest.xml中给上权限和文件保存的位置
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
步骤四:构建页面布局(activity_main.xml主界面布局,以及MusicFragment和VideoFragment音乐和视频界面布局 )
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main"
android:background="@drawable/activity_bg"
android:orientation="horizontal">
<LinearLayout
android:layout_width="70dp"
android:layout_height="150dp"
android:layout_gravity="center_vertical"
android:layout_margin="8dp"
android:background="@drawable/text_bg"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/music"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="音乐"
android:textColor="@color/blue"
android:textSize="30px"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/video"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="视频"
android:textColor="@color/black"
android:textSize="30px"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
<FrameLayout
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="50dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="50dp" />
</LinearLayout>
fragment_music.xml
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SeekBar
android:id="@+id/seekBar"
android:layout_width="600px"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/left"
android:layout_width="70px"
android:layout_height="70px"
android:layout_marginTop="32dp"
app:layout_constraintEnd_toStartOf="@+id/play"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/seekBar"
app:srcCompat="@drawable/left" />
<ImageView
android:id="@+id/play"
android:layout_width="70px"
android:layout_height="70px"
app:layout_constraintBottom_toBottomOf="@+id/left"
app:layout_constraintEnd_toStartOf="@+id/right"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/left"
app:layout_constraintTop_toTopOf="@+id/left"
app:srcCompat="@drawable/selector_play" />
<ImageView
android:id="@+id/right"
android:layout_width="70px"
android:layout_height="70px"
app:layout_constraintBottom_toBottomOf="@+id/play"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/play"
app:layout_constraintTop_toTopOf="@+id/play"
app:srcCompat="@drawable/right" />
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="歌曲名称"
android:textColor="@color/black"
android:textSize="50px"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/seekBar"
app:layout_constraintEnd_toEndOf="@+id/seekBar"
app:layout_constraintStart_toStartOf="@+id/seekBar" />
</androidx.constraintlayout.widget.ConstraintLayout>
fragment_video.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".VideoFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_video"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2" />
</FrameLayout>
外加item_video.xml用来设置展示视频的样式
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="20px"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_img"
android:layout_width="match_parent"
android:layout_height="90dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_launcher_background" />
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:padding="5dp"
android:text="视频名称"
android:textColor="@color/black"
android:textSize="16dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
步骤五:构建完页面布局就要针对页面布局接下来处理逻辑
1:MainActivity代码的构建是为了实现fragment的切换
public class MainActivity extends AppCompatActivity {
private com.example.musicandvideo.databinding.ActivityMainBinding binding;
private MusicFragment musicFragment;
private VideoFragment videoFragment;
private FragmentManager supportFragmentManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
initView();
}
private void initView() {
seleFragment(new MusicFragment());
binding.music.setOnClickListener(v -> {
setAllTextColor();
binding.music.setTextColor(getResources().getColor(R.color.blue));
seleFragment(new MusicFragment());
});
binding.video.setOnClickListener(v -> {
setAllTextColor();
binding.video.setTextColor(getResources().getColor(R.color.blue));
seleFragment(new VideoFragment());
});
}
public void seleFragment(Fragment fragment) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(binding.fragment.getId(), fragment).commit();
}
private void setAllTextColor() {
binding.music.setTextColor(getResources().getColor(R.color.black));
binding.video.setTextColor(getResources().getColor(R.color.black));
}
- 类定义:
MainActivity
类继承自AppCompatActivity
,这是实现活动(Activity)的标准方式,提供向后兼容的Activity功能。
- 成员变量:
binding
:使用ActivityMainBinding
类(由View Binding生成)来访问布局中的视图,而不需要使用findViewById
。musicFragment
和videoFragment
:这两个变量预期用于存储MusicFragment
和VideoFragment
的实例,但在提供的代码段中未被使用。supportFragmentManager
:用于管理Fragment事务的FragmentManager
实例,但在提供的代码段中未被初始化或使用;实际上,通过getSupportFragmentManager()
方法可以直接获取。
- onCreate方法:
- 这是Activity生命周期中的一个关键方法,用于初始化Activity。
EdgeToEdge.enable(this);
:这行代码可能是为了启用全屏模式或类似功能,但EdgeToEdge
不是Android SDK的一部分,可能是某个库或项目中自定义的类。- 使用View Binding来绑定布局,并设置活动的内容视图。
- 通过
ViewCompat.setOnApplyWindowInsetsListener
设置窗口内边距监听器,用于调整视图的内边距以适应系统栏(如状态栏和导航栏)。
- initView方法:
- 初始化视图,包括设置默认显示的Fragment(音乐Fragment)和设置音乐、视频按钮的点击监听器。
- 当点击音乐或视频按钮时,首先将所有按钮的文本颜色设置为黑色,然后将被点击按钮的文本颜色设置为蓝色,并替换当前显示的Fragment。
- seleFragment方法:
- 接受一个
Fragment
对象作为参数,并使用FragmentTransaction
来替换当前显示的Fragment。 - 这里使用了
binding.fragment.getId()
来获取Fragment容器的ID,这表明布局文件中应该有一个具有相应ID的视图容器用于承载Fragment。
- 接受一个
- setAllTextColor方法:
- 将音乐和视频按钮的文本颜色设置为黑色。
注意:
- 在
seleFragment
方法中,每次点击按钮时都会创建新的MusicFragment
或VideoFragment
实例。这通常不是最佳实践,因为它会导致不必要的Fragment创建和销毁,可能导致性能问题。更好的做法是使用FragmentManager
的findFragmentByTag
或findFragmentById
方法来检查Fragment是否已存在,如果存在则直接使用它,否则创建新的实例。 supportFragmentManager
变量在代码中未被使用,可以直接在seleFragment
方法中通过getSupportFragmentManager()
调用获取FragmentManager
。
2:实现音乐播放界面MusicFragment.java
public class MusicFragment extends Fragment {
private FragmentMusicBinding binding;
private static final int STORAGE_PERMISSION_CODE = 1;
private List<File> audioFiles;
private int currentSongIndex = 0;
private MediaPlayer mediaPlayer;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentMusicBinding.inflate(inflater, container, false);
View view = binding.getRoot();
// 初始化点击事件
binding.left.setOnClickListener(v -> playPreviousSong());
binding.right.setOnClickListener(v -> playNextSong());
binding.play.setOnClickListener(v -> togglePlayPause());
// 申请权限
if (checkStoragePermission()) {
loadAudioFiles();
} else {
requestStoragePermission();
}
return view;
}
// 检查存储权限是否已授予
private boolean checkStoragePermission() {
return ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
// 申请存储权限
private void requestStoragePermission() {
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, STORAGE_PERMISSION_CODE);
}
// 处理权限申请结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == STORAGE_PERMISSION_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限授予后加载音频文件
loadAudioFiles();
} else {
binding.left.setOnClickListener(null);
binding.right.setOnClickListener(null);
binding.play.setOnClickListener(null);
Toast.makeText(getContext(), "存储权限被拒绝,无法加载音频文件", Toast.LENGTH_SHORT).show();
}
}
}
// 动态加载指定文件夹中的音频文件
private void loadAudioFiles() {
File downloadFolder = new File(Environment.getExternalStorageDirectory() + File.separator + "Download");
audioFiles = new ArrayList<>();
if (downloadFolder.exists() && downloadFolder.isDirectory()) {
File[] files = downloadFolder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && file.getName().endsWith(".mp3")) {
audioFiles.add(file);
}
}
}
if (!audioFiles.isEmpty()) {
playSong(audioFiles.get(currentSongIndex));
} else {
Toast.makeText(getContext(), "没有找到音频文件", Toast.LENGTH_SHORT).show();
}
}
}
// 播放音频文件
private void playSong(File audioFile) {
if (mediaPlayer != null) {
mediaPlayer.release();
}
mediaPlayer = MediaPlayer.create(getContext(), Uri.fromFile(audioFile));
binding.tvName.setText(audioFile.getName());
mediaPlayer.setOnPreparedListener(mp -> {
binding.seekBar.setMax(mediaPlayer.getDuration());
binding.play.setSelected(false); // 确保初始状态为暂停状态
binding.seekBar.setProgress(0); // 进度条初始化为0
});
mediaPlayer.setOnCompletionListener(mp -> playNextSong());
// 更新进度条
binding.seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
mediaPlayer.seekTo(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
// 播放/暂停切换
private void togglePlayPause() {
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying()) {
mediaPlayer.pause();
binding.play.setSelected(false); // 更新按钮状态
binding.seekBar.removeCallbacks(this::updateSeekBar); // 停止进度条更新
} else {
mediaPlayer.start();
binding.play.setSelected(true);
updateSeekBar(); // 继续播放时重新启动进度条更新
}
}
}
// 播放上一首歌曲
private void playPreviousSong() {
if (!audioFiles.isEmpty()) {
currentSongIndex = (currentSongIndex - 1 + audioFiles.size()) % audioFiles.size();
playSong(audioFiles.get(currentSongIndex));
}
}
// 播放下一首歌曲
private void playNextSong() {
if (!audioFiles.isEmpty()) {
currentSongIndex = (currentSongIndex + 1) % audioFiles.size();
playSong(audioFiles.get(currentSongIndex));
}
}
// 更新进度条
private void updateSeekBar() {
if (mediaPlayer != null) {
binding.seekBar.setProgress(mediaPlayer.getCurrentPosition());
if (mediaPlayer.isPlaying()) {
binding.seekBar.postDelayed(this::updateSeekBar, 1000); // 每秒更新一次进度条
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
}
binding.seekBar.removeCallbacks(this::updateSeekBar); // 清除所有的回调
}
3:实现视频播放器部分代码
VideoAdapter.java
public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoViewHolder> {
private List<File> videoFiles;
private OnVideoClickListener onVideoClickListener;
public VideoAdapter(List<File> videoFiles, OnVideoClickListener onVideoClickListener) {
this.videoFiles = videoFiles;
this.onVideoClickListener = onVideoClickListener;
}
@NonNull
@Override
public VideoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new VideoViewHolder(ItemVideoBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(@NonNull VideoViewHolder holder, int position) {
File videoFile = videoFiles.get(position);
holder.itemView.setOnClickListener(v -> onVideoClickListener.onVideoClick(videoFile));
holder.onBind(videoFile);
}
@Override
public int getItemCount() {
return videoFiles.size();
}
public static class VideoViewHolder extends RecyclerView.ViewHolder {
ItemVideoBinding binding;
public VideoViewHolder(@NonNull ItemVideoBinding itemView) {
super(itemView.getRoot());
binding = itemView;
}
public void onBind(File file) {
Bitmap thumbnail = getVideoThumbnail(file.getPath());
if (thumbnail != null) {
binding.ivImg.setImageBitmap(thumbnail);
}
binding.tvName.setText(file.getName());
}
// 获取视频封面缩略图
private Bitmap getVideoThumbnail(String filePath) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
retriever.setDataSource(filePath);
// 提取视频的第一帧作为缩略图
return retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
retriever.release();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
4:VideoFragment代码实现
public class VideoFragment extends Fragment {
private FragmentVideoBinding binding;
private List<File> videoList = new ArrayList<>();
private VideoAdapter videoAdapter;
private static final String[] REQUIRED_PERMISSIONS = new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE // 读取外部存储的权限
};
private static final int REQUEST_PERMISSION_CODE = 1;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentVideoBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
videoAdapter = new VideoAdapter(videoList, this::onVideoClick);
binding.rvVideo.setAdapter(videoAdapter);
loadVideoFiles(); // 加载视频文件
}
private void loadVideoFiles() {
if (!hasPermissions()) {
requestPermissions();
return;
}
File downloadFolder = new File(Environment.getExternalStorageDirectory() + File.separator + "Download");
if (downloadFolder.exists() && downloadFolder.isDirectory()) {
File[] videoFiles = downloadFolder.listFiles((dir, name) -> name.endsWith(".mp4") || name.endsWith(".avi") || name.endsWith(".mkv"));
if (videoFiles != null && videoFiles.length > 0) {
videoList.clear();
Collections.addAll(videoList, videoFiles);
videoAdapter.notifyDataSetChanged();
} else {
Toast.makeText(getContext(), "未找到视频文件", Toast.LENGTH_SHORT).show();
}
}
}
private boolean hasPermissions() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(getContext(), permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
private void requestPermissions() {
requestPermissions(REQUIRED_PERMISSIONS, REQUEST_PERMISSION_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUEST_PERMISSION_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadVideoFiles();
} else {
Toast.makeText(getContext(), "无法访问存储,请授予权限", Toast.LENGTH_SHORT).show();
}
}
}
// 点击播放视频
private void onVideoClick(File videoFile) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri videoUri = FileProvider.getUriForFile(requireContext(), getContext().getPackageName() + ".provider", videoFile);
intent.setDataAndType(videoUri, "video/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
}
}
这就是逻辑部分代码最后在
中添加视频音频
标签:视频,试题,void,binding,private,public,android,播放,App From: https://blog.csdn.net/2401_83566316/article/details/142890878