안녕하세요.
안드로이드 스튜디오에서 자바로 음악 플레이어를 빠르게 구현하는 방법을
공유하겠습니다.
내 스마트폰에 저장되어 있는 mp3 파일을 읽어와서 음악을 재생하는 앱입니다.
사전 준비 사항
1. 스틱코드 회원가입
(https://stickode.com/signup.html)
2. 안드로이드 스튜디오에 스틱 코드 설치 및 로그인
(https://stickode.com/howto.html#jetbrains_installation)
3. 스틱코드 접속하여 음악 플레이어 구현하기 포스팅 즐겨찾기 추가
스틱 코드 음악 플레이어 포스팅 URL
https://stickode.com/detail.html?no=1902
1. 스틱코드로 manifest 퍼미션 추가
안드로이드 스튜디오에 스틱 코드 로그인이 되어 있다면,
mani 만 쳐도 포스트에 등록되어 있는 코드 목록이 보이는 것을 확인할 수 있습니다.
manifest 퍼미션 추가 코드를 불러오시면 됩니다.
2. main activity 추가
main activity 클릭하면
main activity 코드가 불러와 집니다.
3. Music Player 추가
- 새로운 activity 추가하는 방법 (java파일이 있는 폴더 오른쪽 마우스 클릭)
New -> Activity -> Empty Activity
Activity Name 부분에 MusicPlayerActivity 입력 후 Finish 클릭
새로 만들어진 MusicPlayer.java 파일에서 music 단어를 치면
music player activity 목록이 나옵니다.
해당 목록을 클릭하면, 자바 코드가 불러와집니다.
4. xml 파일 코드 추가
xml 파일 코드 불러오기 명칭 (mxl 파일명 => 스틱 코드 포스팅 소스코드 명)
- activity_main.xml => main_xml
- activity_music_player.xml => music player_xml
- song_layout.xml => song list_xml
5. 앱에서 사용되는 이미지 찾아서 저장하기
앞 곡 재생, 다음 곡 재생, 한곡 반복 재생, 전체곡 반복재생 이미지가 필요합니다.
무료 이미지 다운로드 사이트를 찾으셔서 원하는 이미지를 구하셔서 (구글링 하면 금방 나옵니다.)
drawable 폴더 아래에 이미지 이름만 똑같이 변경해서 넣으시면 됩니다.
테스트할 디바이스를 연결해서
테스트해보시면 됩니다.
6. 전체 소스 코드
- main java
package com.project.musicplayer;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
Button music_player_btn;
private final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 10; //외부 파일 읽기 권한 요청 request 값
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
music_player_btn = findViewById(R.id.musicBtn);
//음악 플레이어 버튼 클릭 리스너 셋팅
//기능: 뮤직플레이어 액티비티로 이동
music_player_btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, MusicPlayerActivity.class);
startActivity(intent);
}
});
//mp3 파일을 스마트폰에서 읽어오기 위해 권한 요청하기
//외부저장소 읽기/쓰기, 사진, 카메라 권한 부여 확인 및 권한요청 코드
if (ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
Log.d("permission","checkSelfPermission");
// 설명이 필요한가?
if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
Manifest.permission.READ_EXTERNAL_STORAGE)) {
Log.d("permission","shouldShowRequestPermissionRationale");
// 사용자에게 설명을 보여줍니다.
// 권한 요청을 다시 시도합니다.
} else {
// 권한요청
Log.d("permission","권한 요청");
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA},
MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
}
}
}
//권한요청을 사용자에게 허락받았는지 못받았는지 결과를 알수 있는 콜백 메서드
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 권한 획득 성공
Log.d("permission","권한 획득 성공");
} else {
// 권한 획득 실패
Log.d("permission","권한 획득 실패");
}
return;
}
}
}
}
- 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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/musicBtn"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="30dp"
android:layout_marginTop="10dp"
android:layout_marginRight="30dp"
android:background="#323151"
android:text="음악 플레이어"
android:textColor="#FFFFFF" />
</LinearLayout>
- music player java
package com.project.musicplayer;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.database.Cursor;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.SeekBar;
import android.widget.TextView;
import java.util.ArrayList;
public class MusicPlayerActivity extends AppCompatActivity implements View.OnClickListener {
public String log_name = "musicplayer";
public ListView listView;
private String songNames[];
private ArrayList<Uri> songs;
private String title = "";
static private TextView titleTv;
static public Button btnPlay;
private Button btnBack, btnFor;
private ImageView forwardIv, backwardIv, repeatIv;
static public SeekBar seekBar;
static public MediaPlayer mediaPlayer;
static private int pos = -1; //노래 포지션
static public boolean bearPhoneOneCheck = false; //bearPhoneOneCheck 이어폰이 한번 뽑히면 true, 그렇지 않으면 false
static public int CheckearPhone = 0; //이어폰을 빼면 0, 연결되어 있으면 1이상 값
static public boolean bpause = false; //일시 정지 버튼을 눌렀다면, true
static public int repeatPlayMode = 0; //반복재생 모드 0: 전곡 반복 재생 / 1: 한곡 반복 재생
private Runnable runnable;
private Handler handler;
private Uri uri;
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music_player);
titleTv = findViewById(R.id.titleTv);
listView = findViewById(R.id.listView);
btnPlay = findViewById(R.id.btnPlay);
btnBack = findViewById(R.id.btnBack);
btnFor = findViewById(R.id.btnFor);
seekBar = findViewById(R.id.seekbar);
forwardIv = findViewById(R.id.forwardIv);
backwardIv = findViewById(R.id.backwardIv);
repeatIv = findViewById(R.id.repeatIv);
handler = new Handler();
btnFor.setOnClickListener(this);
btnBack.setOnClickListener(this);
btnPlay.setOnClickListener(this);
forwardIv.setOnClickListener(this);
backwardIv.setOnClickListener(this);
repeatIv.setOnClickListener(this);
//음악이 재생중이였다면 seekBar, 노래제목 표시하기
if(mediaPlayer != null)
{
if(mediaPlayer.isPlaying())
{
if(!title.equals("")){
titleTv.setSelected(true);
titleTv.setText(title);
}
seekBar.setMax(mediaPlayer.getDuration());
seekBar.setProgress(mediaPlayer.getCurrentPosition());
btnPlay.setText("||");
}
else if(!mediaPlayer.isPlaying())
{
if(!title.equals("")){
titleTv.setSelected(true);
titleTv.setText(title);
}
seekBar.setMax(mediaPlayer.getDuration());
seekBar.setProgress(mediaPlayer.getCurrentPosition());
btnPlay.setText(">");
}
if(repeatPlayMode == 0)
{
repeatIv.setImageResource(R.drawable.repeat_32px);
}
else if(repeatPlayMode == 1)
{
repeatIv.setImageResource(R.drawable.repeat_one_32px);
}
}
// MP3 경로를 가질 문자열 배열.
String[] resultPath = null;
String selectionMimeType = MediaStore.Files.FileColumns.MIME_TYPE + "=?";
// 찾고자하는 파일 확장자명.
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension("mp3");
String[] selectionArgsMp3 = new String[]{ mimeType };
Cursor c = null;
c = getContentResolver().query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Audio.Media._ID,MediaStore.Audio.Media.DISPLAY_NAME}, selectionMimeType, selectionArgsMp3, null);
if (c.getCount() != 0){
ArrayList<Uri> arrayList = new ArrayList<Uri>();
songNames = new String[c.getCount()];
//커서로 조회한 mp3 상대경로 와 mp3 제목을 저장한다.
while (c.moveToNext()) {
//음악파일이 저장되어 있는 상대 경로를 uri로 저장.
Uri contentUri = Uri.withAppendedPath(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
c.getString(c.getColumnIndexOrThrow(MediaStore.Audio.Media._ID))
);
//음악이 저장되어 있는 상대 경로 저장
arrayList.add(contentUri);
//음악 제목 저장
songNames[c.getPosition()] = c.getString(c.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)).replace(".mp3", "");
Log.d(log_name, contentUri.toString());
Log.d(log_name, songNames[c.getPosition()]);
}
Log.d(log_name, String.valueOf(c.getCount()));
Log.d(log_name, String.valueOf(arrayList.size()));
songs = arrayList;
}
//음악 리스트를 위한 어댑터 구현
//listview => song_layout textView 위젯에 음악 개수 만큼 음악 제목을 표시한다.
ArrayAdapter<String> adaper = new ArrayAdapter<String>(getApplicationContext(), R.layout.song_layout
, R.id.textView, songNames);
listView.setAdapter(adaper);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
pos = position;
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
}
titleTv.setSelected(true);
titleTv.setText(songNames[pos]);
title = (String)titleTv.getText();
mediaPlayer = MediaPlayer.create(MusicPlayerActivity.this, songs.get(pos));
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
//곡을 클릭하면 곡의 전체시간을 seekBar에 Max값으로 넣는다.
seekBar.setMax(mp.getDuration());
mediaPlayer.start();
bearPhoneOneCheck = true;
changeSeekbar();
}
});
}
});
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
//seekBar를 누르면 눌린 시간으로 mp3 시간을 이동
mediaPlayer.seekTo(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
private void changeSeekbar(){
//현재 mp3 재싱시간을 seekBar에 넣는다.
seekBar.setProgress(mediaPlayer.getCurrentPosition());
if(mediaPlayer.isPlaying())
{
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
AudioDeviceInfo[] audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
for (AudioDeviceInfo deviceInfo : audioDevices) {
if (deviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
|| deviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
//이어폰이 연결되어 있다면 +1씩 증가
CheckearPhone += 1;
}
}
//이어폰을 빼면 음악 일시정지
if(CheckearPhone == 0){
//bearPhoneOneCheck 이어폰이 한번 뽑히면 true, 그렇지 않으면 false
if(!bearPhoneOneCheck)
{
mediaPlayer.pause();
btnPlay.setText(">");
bearPhoneOneCheck = true;
}
else
{
Log.d(log_name,"이어폰 분리~~~~~~~~~~~~");
btnPlay.setText("||");
runnable = new Runnable() {
@Override
public void run() {
changeSeekbar();
}
};
handler.postDelayed(runnable, 400);
}
}
else
{
CheckearPhone = 0;
Log.d(log_name,"이어폰 착용!!!!!!!!!!!!");
btnPlay.setText("||");
runnable = new Runnable() {
@Override
public void run() {
//이어폰을 다시 연결하면 bearPhoneOneCheck = false 로 변경
//bearPhoneOneCheck 는 이어폰을 연결하여 갑자기 뺏을경우 노래가 멈추게 하기위해 만듬
bearPhoneOneCheck = false;
changeSeekbar();
}
};
handler.postDelayed(runnable, 100);
}
}
else
{
//한곡이 끝나면 자동 재생하려고 했으나, seekBar와 mediaPlayer 시간 값이 정확하게 일지하지 않아
// 200 ms에 오차 범위를 넣었다.
Log.d(log_name,"seekbar 끝남 다음곡 재생 : "+ seekBar.getProgress() + " / " + mediaPlayer.getDuration());
if (!bpause && seekBar.getProgress() >= (mediaPlayer.getDuration() - 1500)) {
Log.d(log_name, "seekbar 끝남 다음곡 재생 : " + seekBar.getProgress() + " / " + mediaPlayer.getDuration());
btnPlay.setText(">");
if (repeatPlayMode == 0) //전곡 반복 재생
{
if (pos != -1 && songNames.length - 1 > pos) {
pos += 1;
playMusic(pos);
} else if (songNames.length - 1 == pos) {
pos = 0;
playMusic(pos);
}
} else if (repeatPlayMode == 1) //한곡 반복 재생
{
playMusic(pos);
}
}
}
}
@Override
public void onClick(View v) {
if(mediaPlayer != null) {
switch (v.getId()) {
case R.id.btnPlay:
if (mediaPlayer.isPlaying()) {
mediaPlayer.pause();
bpause = true;
btnPlay.setText(">");
} else {
mediaPlayer.start();
btnPlay.setText("||");
bpause = false;
bearPhoneOneCheck = true;
changeSeekbar();
}
break;
case R.id.btnFor:
mediaPlayer.seekTo(mediaPlayer.getCurrentPosition() + 5000);
break;
case R.id.btnBack:
mediaPlayer.seekTo(mediaPlayer.getCurrentPosition() - 5000);
break;
case R.id.forwardIv:
if(pos != -1 && songNames.length - 1 > pos)
{
pos += 1;
playMusic(pos);
}
else if(songNames.length - 1 == pos)
{
pos = 0;
playMusic(pos);
}
break;
case R.id.backwardIv:
if(pos > 0)
{
pos -= 1;
playMusic(pos);
}
else if(pos == 0)
{
pos = songNames.length - 1;
playMusic(pos);
}
break;
case R.id.repeatIv:
if(repeatPlayMode == 0)
{
repeatPlayMode = 1;
repeatIv.setImageResource(R.drawable.repeat_one_32px);
}
else if(repeatPlayMode == 1)
{
repeatPlayMode = 0;
repeatIv.setImageResource(R.drawable.repeat_32px);
}
break;
}
}
}
private void playMusic(int position)
{
Log.d(log_name,"playMusic position: " + position);
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
}
Log.d(log_name,"playMusic uri: " + songs.get(position).toString());
titleTv.setSelected(true);
titleTv.setText(songNames[position]);
title = (String)titleTv.getText();
mediaPlayer = MediaPlayer.create(MusicPlayerActivity.this, songs.get(position));
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
//곡을 클릭하면 곡의 전체시간을 seekBar에 Max값으로 넣는다.
seekBar.setMax(mp.getDuration());
mediaPlayer.start();
changeSeekbar();
}
});
}
}
- music player 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=".MusicPlayerActivity">
<TextView
android:id="@+id/titleTv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
android:layout_marginRight="10sp"
android:gravity="center_vertical"
android:textSize="20sp"
android:text="제목"
android:textColor="#000000"
android:singleLine="true"
android:marqueeRepeatLimit="marquee_forever"
android:ellipsize="marquee"
android:scrollHorizontally="true"
app:layout_constraintBottom_toTopOf="@+id/seekbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnBack"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="-5"
android:textSize="20dp"
app:layout_constraintBottom_toTopOf="@+id/listView"
app:layout_constraintEnd_toStartOf="@+id/btnPlay"
app:layout_constraintStart_toEndOf="@+id/backwardIv"
app:layout_constraintTop_toBottomOf="@+id/seekbar" />
<ImageView
android:id="@+id/backwardIv"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="center"
android:src="@drawable/backward_32px"
app:layout_constraintBottom_toTopOf="@+id/listView"
app:layout_constraintEnd_toStartOf="@+id/btnBack"
app:layout_constraintHorizontal_bias="0.4"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/seekbar" />
<Button
android:id="@+id/btnPlay"
android:layout_width="50dp"
android:layout_height="50dp"
android:text=">"
android:textSize="20dp"
app:layout_constraintBottom_toTopOf="@+id/listView"
app:layout_constraintEnd_toStartOf="@+id/btnFor"
app:layout_constraintStart_toEndOf="@+id/btnBack"
app:layout_constraintTop_toBottomOf="@+id/seekbar" />
<ImageView
android:id="@+id/forwardIv"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="center"
android:src="@drawable/forward_32px"
app:layout_constraintBottom_toTopOf="@+id/listView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btnFor"
app:layout_constraintTop_toBottomOf="@+id/seekbar" />
<Button
android:id="@+id/btnFor"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="+5"
android:textSize="20dp"
app:layout_constraintBottom_toTopOf="@+id/listView"
app:layout_constraintEnd_toStartOf="@+id/forwardIv"
app:layout_constraintStart_toEndOf="@+id/btnPlay"
app:layout_constraintTop_toBottomOf="@+id/seekbar" />
<ImageView
android:id="@+id/repeatIv"
android:layout_width="30dp"
android:layout_height="30dp"
android:scaleType="center"
android:src="@drawable/repeat_32px"
app:layout_constraintBottom_toTopOf="@+id/listView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/forwardIv"
app:layout_constraintTop_toBottomOf="@+id/seekbar" />
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.169" />
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="350sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- song list xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:id="@+id/textView"
android:textSize="20sp"
android:layout_marginLeft="10sp"
android:layout_marginRight="10sp"
android:textColor="#000000"
android:gravity="center_vertical"
android:marqueeRepeatLimit="marquee_forever"
android:ellipsize="marquee"
android:scrollHorizontally="true"/>
</LinearLayout>
'안드로이드 자바' 카테고리의 다른 글
[Java][Android] 안드로이드 - Radio Button, Radio Group 사용법 (0) | 2021.02.22 |
---|---|
[Java][Android] 당겨서 새로고침 빠르게 구현하기 (2) | 2021.02.20 |
[Java][Android] 안드로이드 뷰페이저 (0) | 2021.02.17 |
[Java][Android] 바텀 네비게이션 with 프래그먼트 만들기 (6) | 2021.02.16 |
[Java][Android] 실시간 이메일 정규식 체크 기능을 만들어 보자 (0) | 2021.02.15 |