1. [과제] RecyclerView를 활용하여 정보를 입력한 뒤 '추가' 버튼 누르면 아래의 리스트에 추가되기
** manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.a008_practice">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
** activity_main 레이아웃
<?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"
tools:context=".MainActivity">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="정보 추가"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TableLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="이름 : "
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<EditText
android:id="@+id/etName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="이름을 입력하세요"
android:inputType="textPersonName" />
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="나이 : "
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<EditText
android:id="@+id/etAge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="나이를 입력하세요"
android:inputType="number" />
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvAddress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="주소 : "
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<EditText
android:id="@+id/etAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="주소를 입력하세요"
android:inputType="textPostalAddress" />
</TableRow>
</TableLayout>
<Button
android:id="@+id/btnInsert"
style="@style/TextAppearance.AppCompat.Display1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="추가"
android:background="#FFEB3B"
android:layout_margin="3dp"
android:textColor="#000000" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
** student_item 레이아웃
<?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="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="183dp"
app:cardBackgroundColor="#FFFFFFFF"
app:cardCornerRadius="10dp"
app:cardElevation="5dp"
app:cardUseCompatPadding="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/nameTitle"
android:layout_width="95dp"
android:layout_height="30dp"
android:layout_marginStart="25dp"
android:layout_marginTop="25dp"
android:src="@drawable/name_img"
app:layout_constraintEnd_toStartOf="@+id/tvName"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvName"
android:layout_width="200dp"
android:layout_height="30dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="4dp"
android:text="name"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="#000000"
app:layout_constraintEnd_toStartOf="@+id/btnDelItem"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ageTitle"
android:layout_width="95dp"
android:layout_height="30dp"
android:layout_marginStart="25dp"
android:layout_marginTop="15dp"
android:src="@drawable/age_img"
app:layout_constraintEnd_toStartOf="@+id/tvAge"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/nameTitle" />
<TextView
android:id="@+id/tvAge"
android:layout_width="210dp"
android:layout_height="30dp"
android:layout_marginTop="15dp"
android:layout_marginEnd="44dp"
android:text="age"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="#000000"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvName" />
<ImageView
android:id="@+id/addressTitle"
android:layout_width="95dp"
android:layout_height="32dp"
android:layout_marginStart="25dp"
android:layout_marginTop="15dp"
android:src="@drawable/address_img"
app:layout_constraintEnd_toStartOf="@+id/tvAddress"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ageTitle" />
<TextView
android:id="@+id/tvAddress"
android:layout_width="210dp"
android:layout_height="30dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="44dp"
android:layout_marginBottom="25dp"
android:text="address"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="#000000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvAge"
app:layout_constraintVertical_bias="0.466" />
<ImageButton
android:id="@+id/btnDelItem"
android:layout_width="37dp"
android:layout_height="39dp"
android:layout_marginTop="15dp"
android:layout_marginEnd="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@android:drawable/ic_delete" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
** MainActivity 클래스
package com.example.a008_practice;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
StudentBookAdapter adapter;
RecyclerView rv;
EditText etName, etAge, etAddress;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etName = findViewById(R.id.etName);
etAge = findViewById(R.id.etAge);
etAddress = findViewById(R.id.etAddress);
rv = findViewById(R.id.rv);
// RecyclerView 를 사용하기 위해서는 LayoutManager 지정해주어야 한다.
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
rv.setLayoutManager(layoutManager);
// Adapter 객체 생성
adapter = new StudentBookAdapter();
// 추가 버튼, 데이터 생성
Button btnInsert = findViewById(R.id.btnInsert);
btnInsert.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String name = etName.getText().toString();
String age = etAge.getText().toString();
String address = etAddress.getText().toString();
adapter.addItem(new StudentBook(name, age, address));
}
});
rv.setAdapter(adapter); // RecyclerView 에 Adapter 장착
} // end onCreate()
} // and Activity
** StudentBook 클래스
package com.example.a008_practice;
import java.io.Serializable;
// 학생들 명단을 담을 클래스
public class StudentBook implements Serializable {
String name; // 이름
String age; // 나이
String address; // 주소
public StudentBook() {
}
public StudentBook(String name, String age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
// getter, setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
} // end StudentBook
** StudentBoodAdapter 클래스
package com.example.a008_practice;
import android.annotation.SuppressLint;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
// Adapter 객체 정의
// 데이터를 받아서 각 item 별로 View 생성
public class StudentBookAdapter extends RecyclerView.Adapter<StudentBookAdapter.ViewHolder> {
List<StudentBook> items = new ArrayList<StudentBook>();
static StudentBookAdapter adapter;
// Adapter 생성자
public StudentBookAdapter() {this.adapter = this;}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inf = LayoutInflater.from(parent.getContext());
View itemView = inf.inflate(R.layout.studnet_item, parent, false);
return new ViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
StudentBook item = items.get(position); // List<> 의 get()
holder.setItem(item);
}
@Override
public int getItemCount() {
return items.size();
}
// nested class (static inner) 로 ViewHolder 클래스 정의
static class ViewHolder extends RecyclerView.ViewHolder {
// ViewHolder 에 담긴 각각의 View 들을 담을 변수
TextView tvName, tvAge, tvAddress;
ImageButton btnDelItem;
ImageView nameTitle, ageTitle, addressTitle;
// 생성자 필수
public ViewHolder(@NonNull View itemView) {
super(itemView);
// View 객체 가져오기
nameTitle = itemView.findViewById(R.id.nameTitle);
ageTitle = itemView.findViewById(R.id.ageTitle);
addressTitle = itemView.findViewById(R.id.addressTitle);
tvName = itemView.findViewById(R.id.tvName);
tvAge = itemView.findViewById(R.id.tvAge);
tvAddress = itemView.findViewById(R.id.tvAddress);
btnDelItem = itemView.findViewById(R.id.btnDelItem);
// 삭제버튼 누르면 item 삭제되게 하기
btnDelItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
adapter.removeItem(getAdapterPosition()); // 데이터 삭제
// 데이터 변경 (수정, 삭제, 추가) 내역이 adapter 에 반영되어야 정상적으로 동작함!!! ★★★
adapter.notifyDataSetChanged();
}
});
} // end 생성자
// StudentBook 데이터 받아서 멤버 변수 세팅
@SuppressLint("ResourceType")
public void setItem(StudentBook item) {
tvName.setText(item.getName());
tvAge.setText(item.getAge());
tvAddress.setText(item.getAddress());
}
} // end ViewHolder
// 데이터를 다루기 위한 메소드들
// ArrayList 의 메소드들 사용
public void addItem(StudentBook item) {
items.add(item);
}
public void addItem(int position, StudentBook item) {items.add(position, item);}
public void setItems(ArrayList<StudentBook> item) {this.items = item;}
public StudentBook getItem(int position) {return items.get(position);}
public void setItem(int position, StudentBook item) {items.set(position, item);}
public void removeItem(int position) {items.remove(position);}
} // end StudentBookAdapter
** 사진 추가
** 과제하면서 깨달은 점
1) 안드로이드에서 사진 추가할때 소문자로 구성해야 한다는 점
(처음에 대문자로 했다가 앱이 뻗어버림)
2) student_item 레이아웃을 만들면서 Constraint의 layout에서의 Target은 2개 이상 잡아야 한다는 점
(Target을 하나만 잡아서 앱 실행해 보니깐 내가 설정한 위치가 아닌 엉뚱한 곳에서 보여졌음)
2. A009_sound 모듈
1) MainActivity 액티비티, activity_main 레이아웃
[추가] 필요한 음악 다운 받기
: res 선택 후 우클릭 > New > Android Resource Directory 클릭
> Directory name 작성 후 > OK 클릭하면 폴더 생성
> 생성된 폴더에 필요한 음악 복사 붙여넣기
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lec.android.a009_sounds">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SoundPool : 음향효과"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:background="?android:attr/listDivider" />
<Button
android:id="@+id/btnPlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="play 1" />
<Button
android:id="@+id/btnPlay2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="play 2" />
<Button
android:id="@+id/btnPlay3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="play 3" />
<Button
android:id="@+id/btnStop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="STOP" />
</LinearLayout>
package com.lec.android.a009_sounds;
import androidx.appcompat.app.AppCompatActivity;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
/** 음향: SoundPool
* 짧은 음향 리소스(들)을 SoundPool 에 등록(load)하고, 원할때마다 재생(play)
*
* res/raw 폴더 만들고 음향 리소스 추가하기
*/
public class MainActivity extends AppCompatActivity {
private SoundPool sp;
// 음향 리소스 id
private final int [] SOUND_RES = {R.raw.gun, R.raw.gun2, R.raw.gun3};
// 음향 id 값
int [] soundId = new int [SOUND_RES.length];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btnPlay1 = findViewById(R.id.btnPlay);
Button btnPlay2 = findViewById(R.id.btnPlay2);
Button btnPlay3 = findViewById(R.id.btnPlay3);
Button btnStop = findViewById(R.id.btnStop);
// SoundPool 객체 생성
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
// API21 이상에서는 아래와 같이 SoundPool 생성
sp = new SoundPool.Builder().setMaxStreams(10).build();
} else {
sp = new SoundPool(1, // 재생 음향 최대 개수
AudioManager.STREAM_MUSIC, // 재생 미디어 타입
0 // 재생 품질.. (안쓰임. 디폴트 0)
);
}
// SoundPool 에 음향 리소스들을 load
for(int i = 0; i < SOUND_RES.length; i++){
soundId[i] = sp.load(this, // 현재 화면의 제어권자
SOUND_RES[i], // 음원 파일 리소스
1 // 재생 우선순위
);
} // end for
btnPlay1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 음향 재생
sp.play(soundId[0], // 준비한 sound 리소스 id
1, // 왼쪽볼륨 float 0.0 ~ 1.0
1, // 오른쪽 볼륨 float
0, // 우선순위 int
0, // 반복회수 int, 0:반복안함 -1:무한반복
1f // 재생속도 float, 0.5(절반속도) ~ 2.0 (2배속)
);
}
});
btnPlay2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 음향 재생
sp.play(soundId[1], // 준비한 sound 리소스 id
1, // 왼쪽볼륨 float 0.0 ~ 1.0
0, // 오른쪽 볼륨 float
0, // 우선순위 int
2, // 반복회수 int, 0:반복안함 -1:무한반복
2f // 재생속도 float, 0.5(절반속도) ~ 2.0 (2배속)
);
}
});
btnPlay3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 음향 재생
sp.play(soundId[2], // 준비한 sound 리소스 id
0, // 왼쪽볼륨 float 0.0 ~ 1.0
1, // 오른쪽 볼륨 float
0, // 우선순위 int
-1, // 반복회수 int, 0:반복안함 -1:무한반복
0.5f // 재생속도 float, 0.5(절반속도) ~ 2.0 (2배속)
);
}
});
btnStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for(int i = 0; i < soundId.length; i++){
// 음향 정지
sp.stop(soundId[i]);
}
}
});
} // end onCreate()
} // end Actitity
2) Main2Activity 액티비티, activity_main2 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lec.android.a009_sounds">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".Main2Activity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainActivity" />
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="MediaPlayer"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:id="@+id/btnPlay"
android:layout_width="55dp"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_play"
android:text="시작" />
<ImageButton
android:id="@+id/btnPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_pause"
android:text="일시정지" />
<ImageButton
android:id="@+id/btnResume"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_menu_slideshow"
android:text="재시작" />
<ImageButton
android:id="@+id/btnStop"
android:layout_width="57dp"
android:layout_height="match_parent"
android:src="@android:drawable/checkbox_off_background"
android:text="종료" />
</LinearLayout>
<SeekBar
android:id="@+id/sb"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/btnStop"
android:layout_alignLeft="@+id/btnStop"
android:layout_marginTop="18dp" />
</LinearLayout>
package com.lec.android.a009_sounds;
import androidx.appcompat.app.AppCompatActivity;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.SeekBar;
public class Main2Activity extends AppCompatActivity {
private ImageView btnPlay;
private ImageView btnPause;
private ImageView btnResume;
private ImageView btnStop;
SeekBar sb; // 음악 재생위치를 나타내는 시크바
MediaPlayer mp; // 음악 재생을 위한 객체
int pos; // 재생 위치
boolean isTracking = false;
class MyThread extends Thread {
@Override
public void run() {
// SeekBar 가 음악 재생시, 움직이게 하기
while(mp.isPlaying()){ // 현재 재생 중이라면
pos = mp.getCurrentPosition(); // 현재 재생중인 위치 ms (int)
if(!isTracking) {
sb.setProgress(pos); // SeekBar 이동 --> onProgressChanged() 호출함함
}
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
btnPlay = findViewById(R.id.btnPlay);
btnPause = findViewById(R.id.btnPause);
btnResume = findViewById(R.id.btnResume);
btnStop = findViewById(R.id.btnStop);
sb = findViewById(R.id.sb);
btnPlay.setVisibility(View.VISIBLE);
btnPause.setVisibility(View.INVISIBLE);
btnResume.setVisibility(View.INVISIBLE);
btnStop.setVisibility(View.INVISIBLE);
sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
// SB 값 변경될 때마다 호출
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// 음악이 끝까지 연주 완료되었다면
if(seekBar.getMax() == progress && !fromUser) {
btnPlay.setVisibility(View.VISIBLE);
btnPause.setVisibility(View.INVISIBLE);
btnResume.setVisibility(View.INVISIBLE);
btnStop.setVisibility(View.INVISIBLE);
if(mp != null) {mp.stop();}
}
}
// 드래그 시작 (트래킹) 하면 호출
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
isTracking = true;
}
// 드래그 마치면 (드래킹 종료) 하면 호출
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
pos = seekBar.getProgress();
if(mp != null) {mp.seekTo(pos);}
isTracking = false;
}
});
btnPlay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// MediaPlayer 객체 초기화, 재생
mp = MediaPlayer.create(
getApplicationContext(), // 현재 화면의 제어권자
R.raw.chacha); // 음악 파일 리소스
mp.setLooping(false); // true: 무한반복
// 재생이 끝나면 호출되는 콜백 메소드
mp.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Log.d("myapp", "연주종료 " + mp.getCurrentPosition() + " / " + mp.getDuration());
btnPlay.setVisibility(View.VISIBLE);
btnPause.setVisibility(View.INVISIBLE);
btnResume.setVisibility(View.INVISIBLE);
btnStop.setVisibility(View.INVISIBLE);
}
});
mp.start(); // 노래 재생 시작
int duration = mp.getDuration(); // 음악의 재생시간 (ms)
sb.setMax(duration); // SeekBar 의 범위를 음악의 재생 시간으로 설정
new MyThread().start(); // SeekBar 쓰레드 시작
btnPlay.setVisibility(View.INVISIBLE);
btnStop.setVisibility(View.VISIBLE);
btnPause.setVisibility(View.VISIBLE);
}
});
btnStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 음악 종료
pos = 0;
if(mp != null) {
mp.stop(); // 재생 멈춤
mp.seekTo(0); // 음악의 처음으로 이동
mp.release(); // 자원해제
mp = null;
}
sb.setProgress(0); // SeekBar도 초기 위치로
btnPlay.setVisibility(View.VISIBLE);
btnPause.setVisibility(View.INVISIBLE);
btnResume.setVisibility(View.INVISIBLE);
btnStop.setVisibility(View.INVISIBLE);
}
});
// 일시중지
btnPause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pos = mp.getCurrentPosition(); // 현재 재생중이던 위치 저장
mp.pause(); // 일시 중지
btnPause.setVisibility(View.INVISIBLE);
btnResume.setVisibility(View.VISIBLE);
}
});
// 멈춘 시점부터 재시작
btnResume.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mp.seekTo(pos); // 일시 정지시 위치로 이동.
mp.start(); // 재생 시작
new MyThread().start(); // SeekBar 이동(쓰레드)
btnResume.setVisibility(View.INVISIBLE);
btnPause.setVisibility(View.VISIBLE);
}
});
} // end onCreate()
// onPause 단계에서 자원 해제하는 것이 좋다..!!
@Override
protected void onPause() {
super.onPause();
if(mp != null) {
mp.release(); // 자원해제
}
btnPlay.setVisibility(View.VISIBLE);
btnPause.setVisibility(View.INVISIBLE);
btnResume.setVisibility(View.INVISIBLE);
btnStop.setVisibility(View.INVISIBLE);
} // end onPause()
} // end Activity
3. 앱은 사용자에 의해서만 종료할 수 있다(즉, 액티비티는 종료시킬 수 있으나 앱은 종료 불가!!)
4. AsyncTask
: 안드로이드는 메인스레드를 멈출 수 없다
[그렇기 때문에] 메인스레드를 멈추려 하면 에러 발생
** 동기 : 작업이 완료될 때까지 다음 작업을 실행하지 않는다.
** 비동기 : 작업이 시작되면 다음 작업을 진행하고, 작업이 완료된 경우 다시 호출이 된다.
[추가] onPreExecute( ), onProgressUpadate( ), onPostExecute( ) 메소드는 메인 스레드에서 실행되므로
UI 객체에 자유롭게 접근할수 있다
5. a011_handler 모듈
1) MainActivity 액티비티, activity_main 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lec.android.a011_handler">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Thread"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
<TextView
android:id="@+id/tvMainValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="메인스레드 값"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<TextView
android:id="@+id/tvBackValue1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="작업스레드 값1"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/tvBackValue2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="작업스레드 값2"
android:textAppearance="?android:attr/textAppearanceLarge" />
<Button
android:id="@+id/btnIncrease"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="mOnClick"
android:text="메인스레드 값 증가"/>
<Button
android:id="@+id/btnStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="mOnClick2"
android:text="메인스레드 중지"/>
</LinearLayout>
package com.lec.android.a011_handler;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
/** Thread 사용
* Thread 클래스(run 함수의 구현 포함)를 별도로 정의하고,
* Thread의 객체를 메인 Activity내에서 생성하고 Thread를 start시킴.
*
* 일반적인 자바 프로그래밍에서는 메인스레드가 종료되면, 작업스레도도 잘(?) 종료되지만
* 안드로이드 액티비티에선 메인스레드가 종료되도 (심지어 어플이 종료되도) 작업스레드가
* 종료되지 않는 경우가 있습니다. 그래서 setDaemon(true) 메소드를 통해
* 메인스레드와 종료동기화를 시킵니다.
*
* ★ 작업 쓰레드는 Main UI 를 직접 접근할수 없다.★
* ★ 안드로이드는 메인 스레드를 강제로 종료시킬수 없다. ★
*/
public class MainActivity extends AppCompatActivity {
int mainValue = 0;
int backValue1 = 0;
int backValue2 = 0;
TextView tvMainValue;
TextView tvBackValue1;
TextView tvBackValue2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvMainValue = findViewById(R.id.tvMainValue);
tvBackValue1 = findViewById(R.id.tvBackValue1);
tvBackValue2 = findViewById(R.id.tvBackValue2);
BackThread thread1 = new BackThread();
thread1.setDaemon(true); // ★★ [중요] 메인스레드와 종료 동기화..!!
// 자바에서는 자동으로 동기화가 되지만 안드로이드에서는 수동으로 처리해줘야 함
// 만약 안드로이드에서 메인스레드와 종료 동기화에 대한 처리를 하지 않으면
// 메인쓰레드가 죽어도 작업쓰레드는 계속 일하는 상태가 되버린다..!!
thread1.start(); // 작업스레드 시작
BackRunnable runnable = new BackRunnable();
Thread thread2 = new Thread(runnable);
thread2.setDaemon(true);
thread2.start();
} // onCreate()
public void mOnClick(View v) {
mainValue++;
tvMainValue.setText("메인스레드 값 : " + mainValue);
tvBackValue1.setText("작업스레드1 값 : " + backValue1);
tvBackValue2.setText("작업스레드2 값 : " + backValue2);
} // end mOnClick()
// 안드로이드는 메인 쓰레드를 강제 종료시킬 수 없다.
public void mOnClick2(View v) {
Thread.currentThread().stop(); // Caused by: java.lang.UnsupportedOperationException
} // end mOnClick2()
// 1. Thread 를 상속받은 작업스레드
class BackThread extends Thread {
@Override
public void run() {
while (true) {
backValue1++; // 작업스레드 값 증가
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 작업쓰레드에서 메인 UI 직접 건드릴 수 없다.
//tvBackValue1.setText("" + backValue1); // android.view.ViewRootImpl$CalledFromWrongThreadException
}
}
} // end class BackThread
// 2. Runnable 을 implement
class BackRunnable implements Runnable {
@Override
public void run() {
while (true) {
backValue2 += 2; // 작업스레드 값 증가
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} // end class BackRunnable
} // end Activity
2) Main2Activity 액티비티, activity_main2 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lec.android.a011_handler">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".Main2Activity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainActivity" />
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Handler"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
<TextView
android:id="@+id/tvMainValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="메인스레드 값"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/tvBackValue1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="작업스레드 값1"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/tvBackValue2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="작업스레드 값2"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/tvBackValue3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="작업스레드 값3"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/tvBackValue4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="작업스레드 값4"
android:textAppearance="?android:attr/textAppearanceLarge" />
<Button
android:id="@+id/btnIncrease"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="mOnClick"
android:text="메인스레드 값 증가"/>
</LinearLayout>
package com.lec.android.a011_handler;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.TextView;
/** Handler
* 자바는 자바가상머신 위에서 자체적으로 스레드를 생성하고 운영하긴 하지만,
* 스레드 부분 만큼은 '운영체제'의 영향을 받는다.
* 안드로이드에서 돌아가는 자바는 결국 '안드로이드 운영체제'의 영향을 받을수 밖에 없는데, ..
* 안드로이드 운영체제의 경우 '작업스레드' 가 '메인스레드'의 변수를 참조하거나 변경을 할수 있어도,
* '메인스레드' 에서 정의된 UI 를 변경할수는 없게 하고 있습니다. --> CalledFromWrongThreadException !! (이전 예제 참조)
*
* 안드로이드에서 '작업 스레드' 가 '메인스레드의 UI' 에 접근(변경/사용) 하려면 Handler 를 사용해야 합니다
* Handler 는 메인스레드와 작업스레드 간에 통신을 할 수 있는 방법입니다ㅣ
*
* 사용 방법:
* ▫ 'Handler 를 생성'한 스레드만이 다른 작업스레드가 전송하는 'Message' 나 'Runnable객체' 를 수신하는 기능을 할 수 있다.
* ▫ Message 전송은 sendMessage()
* ▫ Runnable 전송은 postXXX()
*/
public class Main2Activity extends AppCompatActivity {
int mainValue = 0;
int backValue1 = 0;
int backValue2 = 0;
TextView tvMainValue;
TextView tvBackValue1, tvBackValue2, tvBackValue3, tvBackValue4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
tvMainValue = findViewById(R.id.tvMainValue);
tvBackValue1 = findViewById(R.id.tvBackValue1);
tvBackValue2 = findViewById(R.id.tvBackValue2);
tvBackValue3 = findViewById(R.id.tvBackValue3);
tvBackValue4 = findViewById(R.id.tvBackValue4);
// 방법1, 방법2
// 스레드 생성하고 시작
BackThread1 thread1 = new BackThread1();
thread1.setDaemon(true);
thread1.start();
// 방법3
BackThread3 thread3 = new BackThread3(handler3); // 메인스레드의 Handler 객체를 외부 클래스에 넘겨줌
thread3.setDaemon(true);
thread3.start();
// 방법4
BackThread4 thread4 = new BackThread4(handler4); // 메인스레드의 Handler 객체를 외부 클래스에 넘겨줌
thread4.setDaemon(true);
thread4.start();
} // end onCreate()
public void mOnClick(View v){
mainValue++;
tvMainValue.setText("MainValue:" + mainValue);
}
class BackThread1 extends Thread{
@Override
public void run() {
while(true){
// 방법1) 메인에서 생성된 Handler 객체의 sendEmptyMessage 를 통해 Message 전달
backValue1++;
handler1.sendEmptyMessage(1);
// 방법2) 메인에서 생성된 Handler 객체의 postXXX() 를 통해 Runnable 객체 전달
backValue2 += 2;
handler2.post(new Runnable() {
@Override
public void run() { // Runnable 의 run() 에서 메인 UI 접근
tvBackValue2.setText("BackValue2: " + backValue2);
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} // end while
} // end run()
} // end BackThread1
//---------------------------------------------------------------------------------------------
// 방법1
// '메인스레드' 에서 Handler 객체를 생성한다.
// Handler 객체를 생성한 스레드 만이 다른 스레드가 전송하는 Message나 Runnable 객체를
// 수신할수 있다.
// 아래 생성된 Handler 객체는 handleMessage() 를 오버라이딩 하여
// Message 를 수신합니다.
Handler handler1 = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
if(msg.what == 1){ // Message id 가 1 이면
tvBackValue1.setText("BackValue1:" + backValue1); // 메인스레드의 UI 변경
}
}
};
//----------------------------------------------------------------------
// 방법2
Handler handler2 = new Handler();
// 방법3
Handler handler3 = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
if(msg.what == 0){
// 메세지를 통해 받은 값을 메인 UI 에 출력
tvBackValue3.setText("BackValue3:" + msg.arg1);
}
}
};
// 방법4
Handler handler4 = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
if(msg.what == 0){
tvBackValue4.setText("BackValue4: " + msg.arg1);
}
}
};
} // and Activity
// #3
// 작업스레드가 메인스레드와 완전히 분리되어 있어서 메인스레드에서 생성한 핸들러를 작업스레드에서
// 직접 참조 할수 없을때, Message 생성자 함수로 메세지를 생성하여 보내주면 됩니다.
// 가령 아래와 같이 메인스레드의 핸들러를 직접 사용할수 없는 분리된 작업 스레드
class BackThread3 extends Thread{
int backValue = 0;
Handler handler;
BackThread3(Handler handler) {this.handler = handler;}
@Override
public void run() {
while(true){
backValue += 3;
Message msg = new Message(); //메세지 생성
msg.what = 0; // 메세지 id
msg.arg1 = backValue;
handler.sendMessage(msg); // 메인스레드의 핸들러에 메세지 보내기
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} // end run()
} // end BackThread3
// 방법4 : 메인스레드의 Handler 를 직접 사용할수 없는 분리된 작업 스레드
class BackThread4 extends Thread{
int backValue = 0;
Handler handler;
BackThread4(Handler handler) {this.handler = handler;}
@Override
public void run() {
while(true){
backValue += 4;
// obtain 메소드로 메세지 생성
// obtain(Handler h, int what, int arg1, int arg2)
// Message.obtain(..) ← 다양하게 오버로딩 되어 있슴
Message msg = Message.obtain(handler, 0, backValue, 0);
handler.sendMessage(msg); // 메인스레드의 Handler 에 메세지 보내기
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} // end run()
} // end BackThread4
3) Main3Activity 액티비티, activity_main3 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lec.android.a011_handler">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".Main3Activity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".Main2Activity" />
<activity android:name=".MainActivity" />
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:id="@+id/tvTitle"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="스케쥴링"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
<TextView
android:id="@+id/tvSummary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="업로드를 시작하려면 다음 버튼을 누르세요."
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<Button
android:id="@+id/btnUpload1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="mOnClick1"
android:text="UPLOAD1" />
<Button
android:id="@+id/btnUpload2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="mOnClick2"
android:text="UPLOAD2" />
<Button
android:id="@+id/btnUpload3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="mOnClick3"
android:text="UPLOAD3" />
<Button
android:id="@+id/btnUpload4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="mOnClick4"
android:text="UPLOAD4" />
</LinearLayout>
package com.lec.android.a011_handler;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import java.util.Timer;
import java.util.TimerTask;
/**
* • 작업 스케쥴링:
* ▫ 작업스레드의 실행 시점을 조절하여, 작업 로드가 많은 작업을 나중으로 미룸으로써
* 응용프로그램이 '끊김'없이 실행될수 있도록할수 있다.
*
* • Handler 사용한 구현 방법:
* boolean sendMessageAtTime (Message msg, uptimeMillis)
* boolean sendEmptyMessageAtTime (what, uptimeMillis)
* boolean sendMessageDelayed (Message msg, long delayMillis)
* boolean sendEmptyMessageDelayed (what, long delayMillis)
* boolean postAtTime (Runnable r, uptimeMillis)
* boolean postDelayed (Runnable r, long delayMillis)
* boolean postAtFrontOfQueue(Runnable r)
*
* xxxAtFrontOfQueue – 큐의 가장 앞에 메세지를 삽입합니다.
* xxxAtTime – 지정한 시간으로 설정하여 큐에 삽입합니다.
* xxxDelayed – 현재시간으로부터 지정한 시간만큼 뒤로 설정하여 큐에 삽입합니다.
*
* • java.util.Timer, java.util.TimerTask 사용한 구현 방법:
*
*/
public class Main3Activity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
} // end onCreate()
Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
doUpload(msg.what);
}
};
void doUpload(int n) {
Toast.makeText(getApplicationContext(), n + " : 업로드를 완료했습니다.", Toast.LENGTH_LONG).show();
}
// #1. 메인스레드가 메인스레드 자신에게 메시지 보내기
// sendEmptyMessageDelayed()
public void mOnClick1(View v){
new AlertDialog.Builder(this)
.setTitle("질문1")
.setMessage("업로드 하시겠습니까?")
.setPositiveButton("예", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mHandler.sendEmptyMessageDelayed(1, 3000);
}
})
.setNegativeButton("아니요", null)
.show();
}
/* 예제#2. Handler 로 Runnable 을 지연(delay)하여 보냄(post)
메인스레드가 메인스레드 자신에게 Runnable 을 보내는 경우임
postDelayed(Runnable) 사용
*/
public void mOnClick2(View v) {
new AlertDialog.Builder(this)
.setTitle("질문2")
.setMessage("업로드 하시겠습니까?")
.setPositiveButton("예", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
doUpload(2);
}
}, 3000);
}
})
.setNegativeButton("아니요", null)
.show();
}
// #3. View 에 Runnable 을 담아서 보냄
public void mOnClick3(View v) {
new AlertDialog.Builder(this)
.setTitle("질문3")
.setMessage("업로드 하시겠습니까?")
.setPositiveButton("예", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// View (Button)을 통해서도 Runnable 을 생성해서 보낼 수 있다.
Button btnUpload = findViewById(R.id.btnUpload3);
btnUpload.postDelayed(new Runnable() {
@Override
public void run() {
doUpload(3);
}
}, 3000);
}
})
.setNegativeButton("아니요", null)
.show();
}
// #4. Timer, TimerTask 사용
public void mOnClick4(View v) {
new AlertDialog.Builder(this)
.setTitle("질문4")
.setMessage("업로드 하시겠습니까?")
.setPositiveButton("예", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
// 예약할 작업 내용 기술
mHandler.sendEmptyMessage(4);
}
};
timer.schedule(task, 3000);
}
})
.setNegativeButton("아니요", null)
.show();
}
} // end Activity
4) Main4Activity 액티비티, activity_main4 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lec.android.a011_handler">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".Main4Activity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".Main3Activity" />
<activity android:name=".Main2Activity" />
<activity android:name=".MainActivity" />
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Timer"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
<TextView
android:id="@+id/tvResult1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="결과창1"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/tvResult2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="결과창2"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/tvResult3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="결과창3"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/tvResult4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="결과창4"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/tvResult5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="결과창5"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</LinearLayout>
package com.lec.android.a011_handler;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.Message;
import android.widget.TextView;
import android.widget.Toast;
// TODO
// Value1
// 1 ~ 10 까지 1초 단위로 증가시키기
// 10초에 도달하면 멈춰서 Toast 띄우기
// Message 사용
// Value2
// 1 ~ 20 까지 1초 단위로 증가시키기
// 20초에 도달하면 멈춰서 Toast 띄우기
// Handler 사용
public class Main4Activity extends AppCompatActivity {
int value1 = 0, value2 = 0, value3 = 0, value4 = 0, value5 = 0;
TextView tvResult1, tvResult2, tvResult3, tvResult4, tvResult5;
Handler mHandler2, mHandler3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main4);
tvResult1 = findViewById(R.id.tvResult1);
tvResult2 = findViewById(R.id.tvResult2);
tvResult3 = findViewById(R.id.tvResult3);
tvResult4 = findViewById(R.id.tvResult4);
tvResult5 = findViewById(R.id.tvResult5);
// 방법 #1 핸들러 객체를 외부에서 생성
mHandler1.sendEmptyMessage(0); // 앱 시작과 동시에 핸들러에 메세지 전달
// 방법 #2 handler.postDelayed() 사용
mHandler2 = new Handler();
mHandler2.postDelayed(new Runnable() {
@Override
public void run() {
value2++;
tvResult2.setText("Value2 = " + value2);
if(value2 < 20){
mHandler2.postDelayed(this, 1000);
} else {
Toast.makeText(getApplicationContext(), "Value2 종료", Toast.LENGTH_LONG).show();
}
}
}, 0);
// 방법 #3 메소드 내부에서 생성
mHandler3 = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
value3++;
tvResult3.setText("Value3 = " + value3);
if(value3 < 5){
mHandler3.sendEmptyMessageDelayed(0, 1000);
} else {
Toast.makeText(getApplicationContext(), "Value3 종료", Toast.LENGTH_LONG).show();
}
}
};
mHandler3.sendEmptyMessage(0); // 시작!
// 방법 #3
// 핸들러를 사용하지 않고도 일정시간마다 (혹은 후에) 코스를 수행할수 있도록
// CountDownTimer 클래스가 제공된다.
// '총시간' 과 '인터벌(간격)' 을 주면 매 간격마다 onTick 메소드를 수행한다.
new CountDownTimer(15 * 1000, 1000) {
@Override
public void onTick(long millisUntilFinished) { // 매 간격마다 수행하는 코드
value4++;
tvResult4.setText("Value4 = " + value4);
}
@Override
public void onFinish() { // 종료시 수행하는 코드
Toast.makeText(getApplicationContext(), "Value4 종료", Toast.LENGTH_LONG). show();
}
}.start(); // 타이머 시작
} // end onCreate
Handler mHandler1 = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
value1++;
tvResult1.setText("Value1 = " + value1);
if(value1 < 10){
// 메세지를 처리하고 또다시 핸들러에 메세지 전달 (1000ms 지연)
mHandler1.sendEmptyMessageDelayed(0,1000);
// 첫번째 매개변수는 message 값
// 두번째 매개변수는 millisec
} else {
Toast.makeText(getApplicationContext(), "Value1 종료", Toast.LENGTH_LONG).show();
}
}
};
} // end Activity
5) Main5Activity 액티비티, activity_main5 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lec.android.a011_handler">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".Main5Activity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".Main4Activity" />
<activity android:name=".Main3Activity" />
<activity android:name=".Main2Activity" />
<activity android:name=".MainActivity" />
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="AsyncTask"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
<TextView
android:id="@+id/tvMainValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="메인스레드 값"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/tvBackValue1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="작업스레드 값1"
android:textAppearance="?android:attr/textAppearanceLarge" />
<Button
android:id="@+id/btnIncrease"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="mOnClick"
android:text="메인스레드 값 증가"/>
</LinearLayout>
package com.lec.android.a011_handler;
import androidx.appcompat.app.AppCompatActivity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
/** AsyncTask
* 백그라운드(background) 작업 스레드 수행,
* 좀더 쉽게 스레드 만들고 운영,
* 작업스레드 결과값까지 쉽게 받아볼수 있다.
* 심지어 Handler 없이도 메인 UI 접근 할수 있다!
*
* AsyncTask 의 메소드
* onPreExecute() : 백그라운드 작업 시작하기 전에 호출
* doInBackground() : 백그라운드 작업, 시간이 많이 걸리는 '통신' 작업이나 복잡한 연산 작업등을 (비동기로)수행케 해야 한다.
* onProgressUpdate() : 백그라운즈 작업 도중 (여러번) 호출가능, 중간중간에 UI업데이트시 사용 가능!
* onPostExecute() : doInBackground() 완료되면 호출
*
* AsyncTask<Params, Progress, Result>
* Params: doItBackground 에서 사용할 변수 타입
* Progress: onProgress 에서 사용할 변수의 타입
* Result : onPostExecute 에서 사용할 변수의 타입
*
*/
public class Main5Activity extends AppCompatActivity {
int mainValue = 0;
int backValue1 = 0;
int backValue2 = 0;
TextView tvMainValue;
TextView tvBackValue1, tvBackValue2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main5);
tvMainValue = findViewById(R.id.tvMainValue);
tvBackValue1 = findViewById(R.id.tvBackValue1);
tvBackValue2 = findViewById(R.id.tvBackValue2);
Log.d("myapp", "PRE!!");
BackgroundTask backgroundTask = new BackgroundTask();
backgroundTask.execute(100);
Log.d("myapp", "post!!");
} // end onCreate()
// [장점] Handler 없이도 메인 UI에 접근 가능..!!
// AsyncTask<Params, Progress, Result>
// Params: doItBackground 에서 사용할 변수 타입
// Progress: onProgress 에서 사용할 변수의 타입
// Result : onPostExecute 에서 사용할 변수의 타입
class BackgroundTask extends AsyncTask<Integer, Integer, Integer> {
// 백그라운드 작업 시작하기 전에 호출
@Override
protected void onPreExecute() {
Log.d("myapp", "onPreExecute");
super.onPreExecute();
}
// [이게 본론...!!] 백그라운드 작업. 반드시 구현!
@Override
protected Integer doInBackground(Integer... integers) { // 가변매개변수, integers 는 Integer[]
for(backValue1 = 0; backValue1 < integers[0]; backValue1++) {
if(backValue1 % 10 == 0) {
publishProgress(backValue1); // progress 상태를 update 뽑아냄 --> onProgressUpdate 호출되고 매개변수값 전달됨.
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return backValue1; // onPostExecute 에 넘어가는 값
// ※ doInBackground() 에서 시간이 많이 걸리는 '통신' 작업이나 복잡한 연산 작업등을 (비동기로)수행케 해야 한다.
}
// 백그라운드 작업 도중 (여러번) 호출 가능, 진행 상황 업데이트, 중간 중간 UI 업데이트시 사용
@Override
protected void onProgressUpdate(Integer... values) {
Log.d("myapp", "Progress : " + values[0] + "%"); // publishProgress(i) 가 보낸 값
super.onProgressUpdate(values);
tvBackValue1.setText("onProgressUpdate : " + values[0]); // ★ Handler 없이도 메인 UI 접근 가능!
}
// doInBackground() 완료되면 호출
@Override
protected void onPostExecute(Integer integer) { // doInBackGround 에서 return 한 값을 매개변수로 받는다.
Log.d("myapp", "Result : " + integer);
super.onPostExecute(integer);
tvBackValue1.setText("onPostExecute: " + integer); // ★ Handler 없이도 메인 UI 접근 가능!
}
} // end AsyncTask
public void mOnClick(View v) {
mainValue++;
tvMainValue.setText("MainValue : " + mainValue);
}
} // end Activity
6. a012_Vibrator 모듈
1) MainActivity 액티비티, activity_main 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lec.android.a012_vibrator">
<!-- 진동 기능 사용권한 획득 -->
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Vibrator"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
<Button
android:id="@+id/btnVib1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="진동1" />
<Button
android:id="@+id/btnVib2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="진동2" />
<Button
android:id="@+id/btnVib3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="진동3" />
<Button
android:id="@+id/btnVib4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="진동4" />
</LinearLayout>
package com.lec.android.a012_vibrator;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.os.Vibrator;
import android.view.View;
import android.widget.Button;
// 진동
// 1. 진동 권한을 획득해야한다. AndroidManifest.xml
// manifest에서 진동 기능 사용권한 획득하기
// 2. Vibrator 객체를 얻어서 진동시킨다
public class MainActivity extends AppCompatActivity {
Button btnVib1, btnVib2, btnVib3, btnVib4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnVib1 = findViewById(R.id.btnVib1);
btnVib2 = findViewById(R.id.btnVib2);
btnVib3 = findViewById(R.id.btnVib3);
btnVib4 = findViewById(R.id.btnVib4);
final Vibrator vibrator
= (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
btnVib1.setText("5초진동");
btnVib1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
vibrator.vibrate(5000); // 지정시간동안 진동
}
});
btnVib2.setText("지정한 패턴으로 진동");
btnVib2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long[] pattern = {100, 300, 100, 700, 300, 2000}; // 단위 ms
// 대기, 진동, 대기, 진동, ...
// 짝수인덱스 : 대기
// 홀수인덱스 : 진동
vibrator.vibrate(pattern, // 진동패턴(배열)
-1); // 반복
// 0: 무한반복, -1 : 반복없음
// 양의 정수 : 진동 패턴 배열의 해당 인덱스부터 진동 무한 반복.
}
});
btnVib3.setText("무한반복으로 진동");
btnVib3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
vibrator.vibrate(
new long[] {100, 1000, 100, 500, 100, 500, 100, 1000}
, 0);
}
});
btnVib4.setText("진동 취소");
btnVib4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
vibrator.cancel(); // 진동 취소
}
});
}
}
7. a014_dialog 모뎀
1) MainActivity 액티비티, activity_main, dialog_layout11, dialog_layout12 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lec.android.a014_dialog">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
** activity_main 레이아웃
<?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"
tools:context=".MainActivity" >
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Dialog"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
<Button
android:id="@+id/btnDialog1"
android:layout_width="match_parent"
android:layout_height="72dp"
android:onClick="showDialog1"
android:text="대화상자 1"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<Button
android:id="@+id/btnDialog2"
android:layout_width="match_parent"
android:layout_height="72dp"
android:onClick="showDialog2"
android:text="대화상자 2"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/tvResult"
android:layout_width="match_parent"
android:layout_height="52dp"
android:text="결과창"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</LinearLayout>
** dialog_layout11 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="400dp"
android:layout_height="match_parent"
android:orientation="vertical" >
<!-- 대화상자 레이아웃의 경우
layout_width 를 match_parent 로 하면
너무 좁게 나온다. (함 보자) 적절한 크기로 지정해주는것이 좋다 -->
<ImageView
android:id="@+id/ivDlgBanner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tvDlgTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="이것은 다이얼로그입니다"
android:textAppearance="?android:attr/textAppearanceLarge" />
<EditText
android:id="@+id/etDlgInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10" >
<requestFocus />
</EditText>
<Button
android:id="@+id/btnDlgEvent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="이벤트 처리 해볼께요" />
<RadioGroup
android:id="@+id/radioGroup1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<RadioButton
android:id="@+id/radio0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="RadioButton0" />
<RadioButton
android:id="@+id/radio1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RadioButton1" />
<RadioButton
android:id="@+id/radio2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RadioButton2" />
</RadioGroup>
</LinearLayout>
** dialog_layout12 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ImageView
android:id="@+id/ivPhoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:text="이름을 입력하세요"
android:textAppearance="?android:attr/textAppearanceLarge" />
<EditText
android:id="@+id/etName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/ivPhoto"
android:layout_alignParentRight="true"
android:ems="10"
android:inputType="text" >
<requestFocus />
</EditText>
<Button
android:id="@+id/btnOk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/ivPhoto"
android:text="확인" />
<Button
android:id="@+id/btnCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/btnOk"
android:layout_alignBottom="@+id/btnOk"
android:layout_alignParentRight="true"
android:text="취소" />
</RelativeLayout>
** MainActivity 액티비티
package com.lec.android.a014_dialog;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
// 대화상자 객체
Dialog dlg1;
ImageView ivDlgBanner;
Button btnDlgEvent;
Dialog dlg2;
TextView tvResult;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvResult = findViewById(R.id.tvResult);
// Dialog 클래스로 다이얼로그 객체 생성및 세팅
dlg1 = new Dialog(this); // 다이얼로그 객체 생성
dlg1.setContentView(R.layout.dialog_layout11); // 다이얼로그 화면 등록
// Dialog 안의 View 객체들 얻어오기
ivDlgBanner = dlg1.findViewById(R.id.ivDlgBanner);
btnDlgEvent = dlg1.findViewById(R.id.btnDlgEvent);
btnDlgEvent.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ivDlgBanner.setImageResource(R.drawable.face04);
Toast.makeText(getApplicationContext(), "다이얼로그 버튼을 눌렀어요", Toast.LENGTH_SHORT).show();
}
});
// Activity 에 Dialog 등록하기
dlg1.setOwnerActivity(MainActivity.this);
dlg1.setCanceledOnTouchOutside(true); // 다이얼로그 바깥 영역 클릭시 (혹은 back 버튼 클릭시) hide() 상태가 됨.
// 종료할 것인지 여부, true : 종료됨, false : 종료 안됨.
// #2
dlg2 = new Dialog(this);
dlg2.setContentView(R.layout.dialog_layout12);
dlg2.setOwnerActivity(MainActivity.this);
dlg2.setCanceledOnTouchOutside(false);
final EditText etName = dlg2.findViewById(R.id.etName);
Button btnOk = dlg2.findViewById(R.id.btnOk);
Button btnCancel = dlg2.findViewById(R.id.btnCancel);
btnOk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String str = etName.getText().toString();
tvResult.setText(str);
dlg2.dismiss(); // 다이얼로그 객체 제거
}
});
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dlg2.dismiss();
}
});
// 다이얼로그가 등장할때 호출되는 메소드
dlg2.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
etName.setText("");
}
});
}// end onCreate()
public void showDialog1(View v){
dlg1.show(); // 다이얼로그 띄우기
}
public void showDialog2(View v){
dlg2.show();
}
} // end Activity
'웹_프론트_백엔드 > JAVA프레임윅기반_풀스택' 카테고리의 다른 글
2020.04.22 (0) | 2020.04.22 |
---|---|
2020.04.21 (0) | 2020.04.21 |
2020.04.17 (0) | 2020.04.17 |
2020.04.16 (0) | 2020.04.16 |
2020.04.14 (0) | 2020.04.14 |