Android开发-仿网易云音乐播放器样式设计与实现

前 言

大家平时在听音乐时使用到的网易云音乐 Android 版 App 时有没有发现网易云音乐的 App 样式做的比较好,App 抽屉式菜单栏使用 Android 独有的特性(相对于IOS) Material Design 风格的设计模式,App 整体风格设计样式符合人性设计。那么这篇博客主要讲如何实现仿网易云音乐简易版播放器。

需求分析

要实现仿网易云音乐简易版播放器的功能,需要实现以下几个功能和步骤:

  1. 自定义音乐播放器 View 的样式;
  2. 实现播放和暂停音乐时指针和光盘的动画效果;
  3. 实现音乐后台播放的服务。

自定义音乐播放器 View 的样式以及指针和光盘的动画效果

创建一个 xml 布局 play_music.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"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <!-- 光盘 -->
    <FrameLayout
        android:id="@+id/fl_play_music"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="90dp">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/disc" />

        <!-- CircleImageView -->
        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/iv_icon"
            android:layout_width="210dp"
            android:layout_height="210dp"
            android:layout_gravity="center"
            app:civ_border_color="@android:color/black"
            app:civ_border_width="2dp" />
            
        <ImageView
            android:id="@+id/iv_play"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_gravity="center"
            android:src="@drawable/play_music"
            android:visibility="gone" />

    </FrameLayout>

    <!-- 指针 -->
    <ImageView
        android:id="@+id/iv_needle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginStart="25dp"
        android:src="@drawable/needle" />

</FrameLayout>

布局主要包含播放音乐的光盘和指针以及光盘内音乐封面和播放按钮。

创建自定义 View 文件 PlayMusicView.java,逻辑代码如下:

public class PlayMusicView extends FrameLayout {

    private Context mContext;

    private MusicModel mMusicModel;
    private MusicService.MusicBind mMusicBinder;
    private Intent mServiceIntent;
    private boolean isPlaying, isBindService;
    private View mView;
    private FrameLayout mFlPlayMusic;
    private ImageView mIvIcon, mIvNeedle, mIvPlay;

    private Animation mPlayMusicAnim, mPlayNeedleAnim, mStopNeedleAnim;

    public PlayMusicView(@NonNull Context context) {
        super(context);
        init(context);
    }

    public PlayMusicView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public PlayMusicView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public PlayMusicView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context) {
        // MediaPlayer
        mContext = context;

        mView = LayoutInflater.from(mContext).inflate(R.layout.play_music, this, false);

        mFlPlayMusic = mView.findViewById(R.id.fl_play_music);
        mFlPlayMusic.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                trigger();
            }
        });
        mIvIcon = mView.findViewById(R.id.iv_icon);
        mIvNeedle = mView.findViewById(R.id.iv_needle);
        mIvPlay = mView.findViewById(R.id.iv_play);

        /**
         * 1、定义所需要执行的动画
         *      1、光盘转动的动画
         *      2、指针指向光盘的动画
         *      3、指针离开光盘的动画
         * 2、startAnimation
         */
        mPlayMusicAnim = AnimationUtils.loadAnimation(mContext, R.anim.play_music_anim);
        mPlayNeedleAnim = AnimationUtils.loadAnimation(mContext, R.anim.play_needle_anim);
        mStopNeedleAnim = AnimationUtils.loadAnimation(mContext, R.anim.stop_needle_anim);

        addView(mView);
    }

    /**
     * 切换播放状态
     */
    private void trigger() {
        if (isPlaying) {
            stopMusic();
        } else {
            playMusic();
        }
    }

    /**
     * 播放音乐
     */
    public void playMusic() {
        isPlaying = true;
        mIvPlay.setVisibility(View.GONE);
        mFlPlayMusic.startAnimation(mPlayMusicAnim);
        mIvNeedle.startAnimation(mPlayNeedleAnim);

//        启动服务
        startMusicService();

    }

    /**
     * 停止播放
     */
    public void stopMusic() {
        isPlaying = false;
        mIvPlay.setVisibility(View.VISIBLE);
        mFlPlayMusic.clearAnimation();
        mIvNeedle.startAnimation(mStopNeedleAnim);

        mMusicBinder.stopMusic();
    }

    /**
     * 设置光盘中显示的音乐封面图片
     */
    private void setMusicIcon() {
        Glide.with(mContext)
                .load(mMusicModel.getPoster())
                .into(mIvIcon);
    }

    /**
     * 设置音乐播放模型
     */
    public void setMusic(MusicModel musicModel) {
        this.mMusicModel = musicModel;

        setMusicIcon();
    }


    /**
     * 启动音乐服务
     */
    private void startMusicService() {

        if (mServiceIntent == null) {
            mServiceIntent = new Intent(mContext, MusicService.class);
            mContext.startService(mServiceIntent);
        } else {
            mMusicBinder.playMusic();
        }

        // 当前未绑定,绑定服务,同时修改绑定状态
        if (!isBindService) {
            isBindService = true;
            mContext.bindService(mServiceIntent, conn, Context.BIND_AUTO_CREATE);
        }
    }

    /**
     * 销毁方法,需要在 activity 被销毁的时候调用
     */
    public void destroy() {
        // 如果已绑定服务,则解除绑定,同时修改绑定状态
        if (isBindService) {
            isBindService = false;
            mContext.unbindService(conn);
        }

    }

    ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mMusicBinder = (MusicService.MusicBind) service;
            mMusicBinder.setMusic(mMusicModel);
            mMusicBinder.playMusic();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
}

该逻辑代码主要实现了音乐播放时所要执行的动画效果,比如:光盘转动的动画、指针指向光盘的动画以及指针离开光盘的动画。还有播放和暂停音乐时切换音乐的播放状态样式和后台服务等。

然后在创建一个用来显示播放器界面的 PlayMusicActivity.java 文件,逻辑代码如下:

public class PlayMusicActivity extends AppCompatActivity {

    public static final String NAME = "name";
    public static final String POSTER = "poster";
    public static final String PATH = "path";
    public static final String AUTHOR = "author";

    private String mName;
    private String mPoster;
    private String mPath;
    private String mAuthor;
    private MusicModel mMusicModel;

    @BindView(R.id.iv_bg)
    ImageView mIvBg;
    @BindView(R.id.tv_name)
    TextView mTvName;
    @BindView(R.id.tv_author)
    TextView mTvAuthor;
    @BindView(R.id.play_music_view)
    PlayMusicView mPlayMusicView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_play_music);
        // 隐藏状态栏
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        initView();
    }

    private void initView() {
        ButterKnife.bind(this);

        mName = getIntent().getStringExtra(NAME);
        mPoster = getIntent().getStringExtra(POSTER);
        mPath = getIntent().getStringExtra(PATH);
        mAuthor = getIntent().getStringExtra(AUTHOR);

        mMusicModel = new MusicModel();
        mMusicModel.setName(mName);
        mMusicModel.setPath(mPath);
        mMusicModel.setPoster(mPoster);
        mMusicModel.setAuthor(mAuthor);

        Glide.with(this)
                .load(mMusicModel.getPoster())
                // 设置音乐播放器背景图片的高斯模糊度
                .apply(RequestOptions.bitmapTransform(new BlurTransformation(25, 35)))
                .into(mIvBg);
        mTvName.setText(mMusicModel.getName());
        mTvAuthor.setText(mMusicModel.getAuthor());

        mPlayMusicView.setMusic(mMusicModel);
        mPlayMusicView.playMusic();
    }

    /**
     * 后退按钮点击事件
     */
    public void onBackClick(View view) {
        onBackPressed();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPlayMusicView.destroy();
    }

}

其中的代码 apply(RequestOptions.bitmapTransform(new BlurTransformation(25, 35))) 是使用图片加载框架 Glide 和 glide-transformations 来实现音乐播放器背景图片的高斯模糊效果,其中 BlurTransformation 第一个参数是采集图片的半径,第二个参数是采集率。

实现音乐后台播放的服务

当音乐播放时我们要把音乐播放的操作放在后台服务,我们创建一个音乐服务文件 MusicService.java ,逻辑代码如下:

/**
 * 1、通过Service 连接 PlayMusicView 和 MediaPlayHelper
 * 2、PlayMusicView -- Service:
 * 1、播放音乐、暂停音乐
 * 2、启动Service、绑定Service、解除绑定Service
 * 3、MediaPlayHelper -- Service:
 * 1、播放音乐、暂停音乐
 * 2、监听音乐播放完成,停止 Service
 */
public class MusicService extends Service {

    //    不可为 0
    public static final int NOTIFICATION_ID = 1;

    private MediaPlayerHelp mMediaPlayerHelp;
    private MusicModel mMusicModel;

    public MusicService() {
    }

    public class MusicBind extends Binder {
        /**
         * 设置音乐(MusicModel)
         */
        public void setMusic(MusicModel musicModel) {
            mMusicModel = musicModel;
            startForeground();
        }

        /**
         * 播放音乐
         */
        public void playMusic() {
            /**
             * 1、判断当前音乐是否是已经在播放的音乐
             * 2、如果当前的音乐是已经在播放的音乐的话,那么就直接执行start方法
             * 3、如果当前播放的音乐不是需要播放的音乐的话,那么就调用setPath的方法
             */
            if (mMediaPlayerHelp.getPath() != null
                    && mMediaPlayerHelp.getPath().equals(mMusicModel.getPath())) {
                mMediaPlayerHelp.start();
            } else {
                mMediaPlayerHelp.setPath(mMusicModel.getPath());
                mMediaPlayerHelp.setOnMeidaPlayerHelperListener(new MediaPlayerHelp.OnMeidaPlayerHelperListener() {
                    @Override
                    public void onPrepared(MediaPlayer mp) {
                        mMediaPlayerHelp.start();
                    }

                    @Override
                    public void onCompletion(MediaPlayer mp) {
                        stopSelf();
                    }
                });
            }
        }

        /**
         * 暂停播放
         */
        public void stopMusic() {
            mMediaPlayerHelp.pause();
        }
    }

    @Override
    public IBinder onBind(Intent intent) {

        return new MusicBind();
    }

    @Override
    public void onCreate() {
        super.onCreate();

        mMediaPlayerHelp = MediaPlayerHelp.getInstance(this);
    }

    /**
     * 系统默认不允许不可见的后台服务播放音乐,
     * Notification ,
     */
    /**
     * 设置服务在前台可见
     */
    private void startForeground() {

        /**
         * 通知栏点击跳转的intent
         */
        PendingIntent pendingIntent = PendingIntent
                .getActivity(this, 0, new Intent(this,
                        MainActivity.class), PendingIntent.FLAG_CANCEL_CURRENT);


        /**
         * 创建Notification
         */
        Notification notification = null;
        /**
         * android API 26 以上 NotificationChannel 特性适配
         */
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = createNotificationChannel();
            notification = new Notification.Builder(this, channel.getId())
                    .setContentTitle(mMusicModel.getName())
                    .setContentText(mMusicModel.getAuthor())
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setContentIntent(pendingIntent)
                    .build();
            NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            notificationManager.createNotificationChannel(channel);
        } else {
            notification = new Notification.Builder(this)
                    .setContentTitle(mMusicModel.getName())
                    .setContentText(mMusicModel.getAuthor())
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setContentIntent(pendingIntent)
                    .build();
        }

        /**
         * 设置 notification 在前台展示
         */
        startForeground(NOTIFICATION_ID, notification);

    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    private NotificationChannel createNotificationChannel() {
        String channelId = "CloudMusic";
        String channelName = "CloudMusicTestService";
        String Description = "CloudMusicTest";
        NotificationChannel channel = new NotificationChannel(channelId,
                channelName, NotificationManager.IMPORTANCE_HIGH);
        channel.setDescription(Description);
        channel.enableLights(true);
        channel.setLightColor(Color.RED);
        channel.enableVibration(true);
        channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
        channel.setShowBadge(false);

        return channel;

    }
}

该逻辑代码的主要功能实现了启动音乐 Service 和解绑音乐 Service 的操作以及音乐播放时把音乐播放的状态显示到通知栏里。当开始播放音乐时,如果音乐 Service 进程已经被解绑或者“杀死”掉则重新开启音乐 Service ,如果当前有其它音乐正在播放,那么先结束上一个的音乐播放再进行当前播放当前的音乐等。

界面运行效果图如下:
在这里插入图片描述

apk安装包下载体验地址:

可以扫描以下二维码进行下载安装,或者点击以下链接 http://app.fukaimei.top/CloudMusicTest 进行下载安装体验。
在这里插入图片描述

———————— The end ————————

码字不易,如果您觉得这篇博客写的比较好的话,可以赞赏一杯咖啡吧~~
在这里插入图片描述


Demo程序源码下载地址一(GitHub)
Demo程序源码下载地址二(码云)

发布了73 篇原创文章 · 获赞 157 · 访问量 24万+

猜你喜欢

转载自blog.csdn.net/fukaimei/article/details/105248614