Android实战项目——音乐播放器 由四大组件之一Service、使用Service进行本地通信、Handle消息机制、Android动画共同完成 类似主流音乐APP界面 简单实用

Android实战项目——音乐播放器 由四大组件之一Service、使用Service进行本地通信、Handle消息机制、Android动画共同完成 类似主流音乐APP界面 简单实用

实际开发经常会涉及服务有Handler消息机制,两者具有紧密的联系,本文将通过一个音乐播放器案例演示如何使用服务进行本地通信,让大家更好的理解服务通信在实际开发中的应用。

实现音乐播放器功能的具体步骤如下:

1、创建程序

创建一个名为MusicPlayer的应用程序,指定包名为cn.itcast.musicplayer。

2、导入音乐文件

音频文件一般放在res/raw文件夹【raw文件夹中的文件会被映射到R.java文件中,访问该文件时可直接使用资源ID,即R.id.music(文件名)】中,因此需要在res文件夹中创建一个raw文件夹。首先将Android Studio中的选项卡切换到Project,接着选中程序中的res文件夹,右击选择【New】—>【Directory】选项,创建一个名为raw的文件夹,接着将音乐文件music.mp3(不能为中文)导入到raw文件夹中。

3、导入界面图片

将播放器界面所需的背景图片、音乐图片导入到程序中的drawable文件夹中。

4、放置界面控件

界面实现效果如图
在这里插入图片描述
在activity_main.xml布局文件中,放置1个ImageView控件用于显示界面上的旋转图片,1个SeekBar用于显示音乐播放器的进度条,2个TextView分别用于显示音乐播放的进度时间与音乐的总时间,4个Button控件分别用于显示“播放音乐”按钮、“暂停播放”按钮、“继续播放”按钮、“退出”按钮,完整布局代码如下:

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="center"
    android:background="@drawable/music_bg">

    <ImageView
        android:id="@+id/iv_music"
        android:layout_width="240dp"
        android:layout_height="240dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="15dp"
        android:src="@drawable/music"/>
    <SeekBar
        android:id="@+id/sb"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="8dp"
        android:paddingRight="8dp">
        <TextView
            android:id="@+id/tv_progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="00:00" />
        <TextView
            android:id="@+id/tv_total"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:text="00:00" />
    </RelativeLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button
            android:id="@+id/btn_play"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_margin="8dp"
            android:layout_weight="1"
            android:background="@drawable/btn_bg_selector"
            android:text="播放音乐"/>
        <Button
            android:id="@+id/btn_pause"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_margin="8dp"
            android:layout_weight="1"
            android:background="@drawable/btn_bg_selector"
            android:text="暂停播放"/>
        <Button
            android:id="@+id/btn_continue_play"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_margin="8dp"
            android:layout_weight="1"
            android:background="@drawable/btn_bg_selector"
            android:text="继续播放"/>
        <Button
            android:id="@+id/btn_exit"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_margin="8dp"
            android:layout_weight="1"
            android:background="@drawable/btn_bg_selector"
            android:text="退出"/>
    </LinearLayout>
</LinearLayout>

5、创建背景选择器btn_bg_selector.xml

通过背景选择器实现界面4个按钮背景的四个角是圆角,并且背景在按下与弹起时,背景颜色会有明显区别。选中drawable文件夹,右击选择【New】——>【Drawable resource file】选项,创建一个背景选择器btn_bg_selector.xml,具体代码+注释如下:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">		<!--state_pressed表示按钮的点击状态,值为true时表示按钮被按下;为false时,表示按钮弹起-->
        <shape android:shape="rectangle">	<!--shape用于定义形状,rectangle表示矩形-->
            <corners android:radius="3dp"/>	<!--corners表示定义矩形的四个角为圆角,radius用于设置圆角半径-->
            <solid android:color="#d4d4d4"/><!--solid用于指定矩形内部的填充颜色-->
        </shape>
    </item>
    <item android:state_pressed="false">
        <shape android:shape="rectangle">
            <corners android:radius="3dp"/>
            <solid android:color="#ffffff"/>
        </shape>
    </item>
</selector>

6、创建MusicService服务

由于音乐的加载、播放、暂停以及播放进度条的更新是一件比较耗时的操作,因此需要创建一个MusicService服务来处理这些操作。首先选择cn.itcast.musicplayer包,右击选择【new】—>【Service】—>【Service】选项,创建名为MusicService的服务。具体代码如下:

package cn.itcast.musicplayer;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Message;

import java.util.Timer;
import java.util.TimerTask;

public class MusicService extends Service {
    private MediaPlayer player;
    private Timer timer;
    public MusicService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        return new MusicControl();
    }
    public void onCreate(){
        super.onCreate();
        player = new MediaPlayer();//创建音乐播放器对象
    }
    public void addTimer(){     //添加计时器用于设置音乐播放器中的播放进度条
        if(timer ==null){
            timer = new Timer();       //创建计时器对象
            TimerTask task = new TimerTask() {
                @Override
                public void run() {     //创建一个线程
                    if(player == null) return;
                    //获取歌曲总时长
                    int duration = player.getDuration();
                    //获取播放进度
                    int currentPosition = player.getCurrentPosition();
                    //创建消息对象
                    Message msg = MainActivity.handler.obtainMessage();
                    //将音乐的总时长和播放进度封装至消息对象中
                    Bundle bundle = new Bundle();
                    bundle.putInt("duration",duration);
                    bundle.putInt("currentPosition",currentPosition);
                    msg.setData(bundle);
                    //将消息发送到主线程的消息列表
                    MainActivity.handler.sendMessage(msg);
                }
            };
            //调用Timer对象的schedule()方法执行TimerTask任务
            // 该方法有三个参数:1、要执行的任务;2、开始执行计时任务的5毫秒后第一次执行task任务;3、每隔500毫秒执行一次
            timer.schedule(task,5,500);
        }
    }

    class MusicControl extends Binder{	//实现播放、暂停、继续播放、设置音乐播放进度条的功能
        public void play(){
            try {
                player.reset();     //重置音乐播放器
                //加载多媒体文件
                player = MediaPlayer.create(getApplicationContext(),R.raw.music);
                player.start();//播放音乐
                addTimer();     //添加计时器
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        public void pausePlay(){
            player.pause();     //暂停播放音乐
        }
        public void continuePlay(){
            player.start();     //继续播放音乐
        }
        public void seekTo(int progress){
            player.seekTo(progress);    //设置音乐的播放位置
        }
    }
    public void onDestroy(){
        super.onDestroy();
        if(player == null) return;
        if(player.isPlaying()) player.stop();   //停止播放音乐
        player.release();                       //释放占用的资源
        player = null;                          //将player置为空
    }
}

上述代码中,addTimer()方法用于每隔500毫秒更新音乐播放器的进度条,在该方法中首先创建一个计时器Timer的对象,接着创建一个TimerTask任务task,在该任务中重写了run()方法创建一个线程,在run()方法中通过getDuration()方法getCurrentPosition()方法分别获取歌曲的总时长与歌曲当前的播放进度。getDuration()方法与getCurrentPosition()方法是MediaPlayer常用方法,更多Mediaplayer常用方法见:MediaPlayer常用方法介绍

7、编写界面交互代码

MainActivity实现了音乐文件的播放、暂停播放、继续播放、播放进度的设置、退出音乐播放界面的功能以及实现音乐播放界面4个按钮的点击事件。具体代码如下(带详细注释)

package cn.itcast.musicplayer;

import androidx.appcompat.app.AppCompatActivity;

import android.animation.ObjectAnimator;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static SeekBar sb;
    private static TextView tv_progress, tv_total;
    private ObjectAnimator animator;
    private MusicService.MusicControl musicControl;
    MyServiceConn conn;
    Intent intent;
    private boolean isUnbind = false;//记录服务是否被解绑
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        tv_progress = (TextView) findViewById(R.id.tv_progress);
        tv_total = (TextView) findViewById(R.id.tv_total);
        sb = (SeekBar) findViewById(R.id.sb);
        findViewById(R.id.btn_play).setOnClickListener(this);
        findViewById(R.id.btn_pause).setOnClickListener(this);
        findViewById(R.id.btn_continue_play).setOnClickListener(this);
        findViewById(R.id.btn_exit).setOnClickListener(this);
        intent = new Intent(this, MusicService.class);//创建意图对象
        conn = new MyServiceConn();//创建服务连接对象
        bindService(intent, conn, BIND_AUTO_CREATE);  //绑定服务
        //为滑动条添加事件监听
        sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            //滑动条进度改变时会调用该方法
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if(progress ==seekBar.getMax()){    //当滑动条滑到末端时,结束动画
                    animator.pause();               //停止播放动画
                }
            }
            //滑动条开始滑动时调用
            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }
            //滑动条停止滑动时调用
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                //根据拖动的进度改变音乐播放进度
                int progress = seekBar.getProgress();   //获取seekBar的进度
                musicControl.seekTo(progress);          //改变播放进度
            }
        });
        ImageView iv_music = (ImageView) findViewById(R.id.iv_music);
        animator = ObjectAnimator.ofFloat(iv_music,"rotation",0f,360.0f);
        animator.setDuration(10000);    //动画旋转一周的时间为10秒
        animator.setInterpolator(new LinearInterpolator());
        animator.setRepeatCount(-1);    //-1表示设置动画无线循环
    }

    //创建消息处理器对象
    public static Handler handler = new Handler(){
        //在主线程中处理从子线程发送过来的消息
        public void handleMessage(Message msg){
            Bundle bundle = msg.getData(); //获取从子线程发送过来的音乐播放进度
            int duration = bundle.getInt("duration");               //歌曲的总时长
            int currentPostition = bundle.getInt("currentPosition");//歌曲当前进度
            sb.setMax(duration);             //设置SeekBar的最大值为歌曲总时长
            sb.setProgress(currentPostition);//设置SeekBar当前的进度位置
            //歌曲的总时长转换格式
            int minute = duration / 1000 / 60;
            int second = duration / 1000 % 60;
            String strMinute = null;
            String strSecond = null;
            if (minute < 10) {              //如果歌曲的时间中的分钟小于10
                strMinute = "0" + minute; 	//在分钟的前面加一个0
            } else {
                strMinute = minute + "";
            }
            if (second < 10) {           	//如果歌曲的时间中的秒钟小于10
                strSecond = "0" + second;	//在秒钟前面加一个0
            } else {
                strSecond = second + "";
            }
            tv_total.setText(strMinute + ":" + strSecond);
            //歌曲当前播放时长
            minute = currentPostition / 1000 / 60;
            second = currentPostition / 1000 % 60;
            if (minute < 10) {             //如果歌曲的时间中的分钟小于10
                strMinute = "0" + minute;//在分钟的前面加一个0
            } else {
                strMinute = minute + "";
            }
            if (second < 10) {               //如果歌曲的时间中的秒钟小于10
                strSecond = "0" + second;  //在秒钟前面加一个0
            } else {
                strSecond = second + "";
            }
            tv_progress.setText(strMinute + ":" + strSecond);
        }
    };

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_play:                //播放按钮点击事件
                musicControl.play();           //播放音乐
                animator.start();               //播放动画
                break;
            case R.id.btn_pause:               //暂停按钮点击事件
                musicControl.pausePlay();     //暂停播放音乐
                animator.pause();              //暂停播放动画
                break;
            case R.id.btn_continue_play:     //继续播放按钮点击事件
                musicControl.continuePlay(); //继续播放音乐
                animator.start();              //播放动画
                break;
            case R.id.btn_exit:                //退出按钮点击事件
                unbind(isUnbind);               //解绑服务绑定
                isUnbind = true;                //完成解绑服务
                finish();                         //关闭音乐播放界面
                break;
        }
    }

    private class MyServiceConn implements ServiceConnection {  //用于实现连接服务
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder service) {
            musicControl = (MusicService.MusicControl) service;
        }
        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    }
    private void unbind(Boolean isUnbind){
        if(!isUnbind){                  //判断服务是否解绑
            musicControl.pausePlay();   //暂停播放音乐
            unbindService(conn);        //解绑服务
            stopService(intent);        //停止服务
        }
    }
    protected void onDestroy() {
        super.onDestroy();
        unbind(isUnbind); //解绑服务
    }
}

8、运行程序

运行上诉程序,分别点击界面上的“播放音乐”按钮、“暂停播放”按钮、“继续播放”按钮,可实现音乐的播放、暂停、继续播放的功能。点击界面上的“退出”按钮,可退出音乐播放器界面。运行结果如图所示:
在这里插入图片描述
文件创建地址不明白的可参考下图:
在这里插入图片描述

Handler消息机制:

Handler消息处理首先需要在UI线程中创建一个Handler对象,然后在子线程中调用Handler的SendMessage()方法,接着这个消息会存放在UI线程的MessageQueue中,通过Looper对象取出MessageQueue中的消息,最后分发回Handler的HandleMessage()方法中。
谢谢观看!祝学习进步编程开心~希望可以得到你的赞与关注哟

猜你喜欢

转载自blog.csdn.net/weixin_45080371/article/details/106745120