빠진 부분
7 RecyclerView
11 Service
12 FirebaseCloudMessaging
14 CustomWidget
-
OpenJDK
오픈소스 프로젝트이다
-
Oracle JavaSE
공개버전은 사용해도 되나 2019년 이후에는 기술지원을 받기 위해서는 비용을 지불해야 함
Activity의 코드는 *.java 파일에서 작성해야 하며 Activity에는 반드시 하나의 layout을 지정해 사용해야 한다.
-
폴더 내에 사용하지 않는 xml파일은 있어도 프로젝트 파일 내에 에러가 있으면 컴파일이 안됨.
-
파일 이름에는 영어 소문자와 '_'만 가능하다.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 이부분
}
Layout은 트리 구조로 만들어지고 최상위는 반드시 Layout이어야 하지만 내부에는 위젯 또는 또 다른 레이아웃을 포함할 수 있다.
-
추가 라이브러리를 추가하여 레이아웃을 사용할 수 있다.
-
레이아웃의 깊이가 깊어질 경우 성능이 떨어진다
Relative Layout / Constraint Layout 등을 이용하여 깊이를 줄이는 것이 좋다.
초기 버전의 안드로이드에서는 Left/Right/Top/Bottom
을 사용했지만 최근에 Start/End
로 업데이트 되었다.
-
낮은 버전의 minimum SDK로 프로젝트를 생성할 경우
Left
와Start
속성을 모두 적어줘야 정상동작한다. -
높은 버전은
Start
나Left
하나만 적어도 동작한다.
위젯 또는 레이아웃의 크기이고 반드시 지정해 줘야 하는 속성이다.
-
match_parent
: 부모의 사이즈. -
wrap_content
: 위젯의 크기에 맞게, 내용물의 사이즈 만큼만.
안드로이드에는 기기가 많아서 화면의 사이즈도 다양하다.
pixel을 사용하면 해상도가 바뀌면 사용자가 보는 사이즈가 바뀌게 되어
안드로이드에서 dp라는 개념을 도입하였다.
dp로 사이즈를 설정하면 안드로이드 OS에서 다양한 핸드폰들의 Widget이 비슷한 사이즈가 나오도록 출력해 준다.
자신의 경계에서 내부 요소 사이의 여백
다른 위젯과 자신 사이의 여백
Padding과 Margin은 상/하/좌/우 각각 지정 가능하다.
내부 요소의 정렬
Layout 내에서 자신의 위치를 정의
일부 Layout에서만 사용
-
Linear Layout
한방향으로 위젯을 배치
orientation
속성을 반드시 지정해야 한다.위젯이 있기 떄문에 '못 씀'공간에는 다른 위젯을 둘 수 없다.
<?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"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <Button android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> </LinearLayout>
Button의
layout_width
가match_parent
일 때와wrap_content
일때의 차이
- Relative Layout
부모 또는 다른 위젯을 기준 삼아 상대적인 위치를 지정하여 배치
-
Constraint Layout
각 위젯에 위치에 대한 제약을 지정하여 배치하는 레이아웃
※ xml namespace로 android: 대신 app: 을 사용함
constraintStart_toStartOf
: 자신의 왼쪽과 다른 위젯의 왼쪽을 동일하게 정렬constraintStart_toEndOf
: 자신의 왼족을 다른 위젯의 오른쪽에 둠파란선이
constraintStart_toStartOf
, 빨간선이Start_toEndOf
다음 설명부턴 constraint를 생략하겠다.
각각의 constraint는 중력(스프링)처럼 작동하여
start_toStartOf
와End_toEndOf
를 동시에parent
로 설정하면 중간으로 이동함.-
bios: 좌/우 동시에 제약이 걸리면 (위 상황처럼) bias를 조절하면 당겨지는 힘의 비율을 조절할 수 있다.
app:layout_constraintHorizontal_bias="0.3"
이렇게 하면 위젯이 약간 왼쪽로 이동한다.
-
문자열을 출력하는 위젯
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>
-
textSize: 텍스트 크기
-
textColor: 텍스트 색
-
textAlignment: 텍스트 정렬 방식.
-
visibility: 보이는 여부
-
clickable: 클릭 이벤트를 설정하는지 여부
TextView를 상속받은 위젯
클릭 가능하며 클릭 리스너를 등록하면 사용자의 이벤트를 처리 할 수 있다.
사용자가 버튼을 누른 것을 어떻게 알까?
-
Polling: 일정 간격을 두고 주기적으로 버튼의 상태를 확인
- 주로 하드웨어에서 사용
-
Listener: 특정 동작이나 메시지, 이벤트에 대한 콜백 등록
- 주로 소프트웨어에서 사용
- text: 버튼에 적힐 label
안드로이드에서 Listener를 등록하는 방법은
- Activity가 interface를 직접 구현
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
public void onCreate... {
...
// 버튼의 리스너로 자기 자신을 지정
button.setOnClickListener(this);
}
@Override
public void onClick(View view) {
}
}
-
interface의 익명 객체를 만들어서 사용
// ... // 일반 자바 문법 button.setOnclickListener(new View.OnClickListener() { @Override public void onClick(View view) { } }); // 람다식 button.setOnClickListener((view) => textView.setText("hello"))
인자값이 하나일 때는 괄호 생략 가능. body가 한줄일 경우 중괄호 생략 가능 (Javascript와 같다)
익명 객체(람다식)를 사용하기 위해 해야할 자바 버전 설정
android { // 나머지 compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } }
-
장점: 로그 출력, 메시지 출력 등의 단순 코드인 경우 간단하게 구현 가능
-
단점: 해당 객체를 다른 위젯이 사용할 수 없다.
-
-
interface 변수를 사용하는 방법
// 클래스 내 private View.OnClickListener buttonListener = new View.OnClickListener() { @Override public void onClick(View view) { textView.setText("Listener!"); } }; // 생성자 내 button.setOnClickListener(buttonListener);
-
장점: 다른 위젯에서도 리스너로 등록 가능
-
단점: Acitivty에 속한 메소드가 아니기 때문에 다른 위젯이나 메소드 접근에 제약이 있을 수 있다. (이문제는 익명 객체도 있다.)
-
-
메소드를 레이아웃에서 지정하는 방법
public void customListener(View v) { // 함수명은 자유롭게 가능 }
<Button ... android:onClick="customListener" <!--함수명과 같게-->
-
장점: 버튼 별로 메소드를 추가하고 지정하기 쉽다
-
단점: 리스너를 확인하기 위헤 xml파일까지 검토해야 한다.
클릭 이외의 이벤트에는 사용하기 어렵다.
-
사용자로부터 문자열을 입력 받을 수 있는 위젯
-
inputType: textPassword 등 여러 속성이 있음
-
ems
기존에 width가 wrap_content인 경우 글자를 하나도 입력하지 않으면 width가 거의 없이 출력이 됨.
그 때, 최소한의 너비를 확보하기 위해서 사용 (ems값 기준으로 너비를 계산함)
Toast.makeText(Context, "출력할 문자열", Toast.LENGTH_LONG).show();
Checked/Unchecked 두 가지 상태를 가지며 클릭할 때마다 상태가 바뀌는 버튼
public void onCheckedChange(CompountButton btn, boolean isChecked)
이벤트가 존재한다. 사용은 기존 리스너처럼 사용.
UI의 출력 형태만 다를 뿐 내부적인 동작은 Toggle Button과 동일하다
Toggle Button과 같은 리스너를 사용한다.
UI에 출력되는 모습은 CheckBox와 비슷하지만 실제 동작은 Button과 유사하다.
클릭하면 선택되지만 다시 클릭한다고 해서 선택이 해제되지 않는다.
Button과 같은 onClickListener를 사용함
RadioGroup의 자식으로 배치해야 동시 선택이 안된다
<RadioGroup ...>
<RadioButton ...
android:text="One"/>
<RadioButton ...
android:text="Two"/>
<RadioButton ...
android:text="Three"/>
</RadioGroup>
이렇게 사용해야 1또는 2로 선택이 된다.
진척도를 나타내는 위젯
Bar타입과 원형 타입이 있다.
-
Bar: 전체 단계 중 진척 단계를 표현
-
원형: 동그라미 애니메이션을 하는 위젯
Bar 타입
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal" <!--이부분-->
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
원형 타입
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle" <!--이부분-->
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Bar 타입의 메소드
-
incrementProgressBy
: 현재 값에 해당 수를 더함 -
setProgress
: 주어진 값으로 변경
사용자가 드래그하거나 클릭하여 포인팅 지점을 설정 가능한 위젯
연속된 값을 사용하는 타입 (default)와 불연속 값을 사용하는 타입 (discrete)이 있다.
기본 Seek Bar
<SeekBar
...
android:max="10" <!-- seekbar max 설정 -->
/>
불연속 타입 (discrete)
<SeekBar
...
style="@style/Widget.AppCompat.SeekBar.Discrete"
/>
이벤트는 OnSeeKBarChangeListener
이다.
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
if(b){ // b는 사용자에 의해서 값이 바뀌었는지 여부를 나타내는 값이다.
progressBar2.setProgress(i); // i는 값
}
}
화면에 그림을 출력하는 위젯
출력을 원하는 원본 그림을 속성 값으로 줘야 함
png, jpg를 출력할 수 있으며 SVG파일을 불러와서 사용한 xml파일도 출력 가능하다.
-
res/drawable 폴더에 이미지를 불러옴
-
layout 에서 불러와서 사용 (예시)
<ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/seekBar2" app:srcCompat="@drawable/coffee_icon_64" />
SVG 불러오기는 스킵.
-
Activity: UI가 있는 단일 화면, 하나의 Activity는 하나의 기능을 가짐
하나의 앱에 있어도 각 Activity는 독립적이며, 내 앱이 허용할 경우 다른 앱에서 내 앱의 Activity 중 하나만 골라서 실행 가능.
-
Service
백그라운드에서 실행하며 UI를 제공하지 않음
사용하는 경우들
-
오래 걸리는 작업을 실행
-
Activity가 꺼진 후에도 수행해야 할 작업
-
-
BroadCast Receiver
시스템이 보내는 broadcast를 수신한다. (즉 시스템이 보내는 메시지를 받음)
예를 들어 배터리 잔량이 부족함, 불루투스 기능이 꺼짐 등이 있다.
-
Content Privider
내 앱이 가진 Content(파일 또는 SQLite에 저장된 데이터)를 다른 앱이 조회하거나 사용할 수 있도록 제공 해 준다. 주로 주소록, 사진 음악 앱에서 제공함.
-
onCreate()
Activity가 최초로 실행될 때 한 번 호출됨
레이아웃 지정과 딱 한번 초기화할 코드 작성
-
onStart()
Activity가 화면에 출력되는 시점
-
onResume()
사용자와 상호작용이 가능하게 될 때
-
onPause()
Activity가 Foreground(제일 위)에 있지 않게 될 때.
화면 위에 다른 컴포넌트가 출력되어 사용자의 입력을 받지 못하게 됐을 때.
-
onStop()
Activty가 화면에 더 이상 나오지 않게 될 때
-
onDestroy()
Activity가 종료되거나 완전히 새로 로딩되어야 할 때 (화면 회전 등)
안드로이드에서는 Activity를 사용자가 직접 생성할 수 없다.
MyActivity myActivity = new Activity(); // 사용 안됨
이렇게 사용할 수가 없다.
안드로이드에게 Activity생성을 요청하여 요청이 처리되면 Activity가 생성된다.
이러한 요청을 "Intent"객체를 이용해서 작성한다.
Acitivty, Service, Broadcast Receiver 등 모든 안드로이드 컴포넌트가 동일하게 Intent를 사용해서 생성되야함
장점: 다른 앱의 Activity도 동일하게 호출 가능
-
명시적 Intent: 내 App내의 Activity를 class명과 함께 요청하는 것.
-
묵시적 Intent: 내가 필요한 동작을 지정하여 요청하는 것.
사용자가 앱을 선택하여 실행하야 할 경우
ex) 링크 공유하기, 전화하기
AndroidManifest.xml 수정
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.andrstudy.activities">
<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>
<!--이부분 추가-->
<activity android:name=".SecondActivity"></activity>
</application>
</manifest>
Intent i = new Intent(this, SecondActivity.class);
startActivity(i);
Activity를 호출하면 Stack에 쌓임 (즉 현재 페이지가 바뀌는 것이 아니라 액티비티가 위에 쌓임)
이후 사용자가 뒤로가기 키를 누르거나 액티비티 함수에서 finish()
를 호출하면 Stack에서 나옴.
-
단방향 전달
-
보내는 쪽
Intent i = new Intent(this, SecondActivity.class); i.putExtra("message", "Hello!!"); startActivity(i);
-
받는 쪽
String message = getIntent().getStringExtra("message"); if(message != null) { // 받은 message 사용 }
-
-
양방향 전달
-
Request Number 부여
// MainActivity.java private static final int REQ_THIRD = 123;
-
MainActivity에서 호출
// MainActivity.java Intent i = new Intent(this, SecondActivity.class); startActivityForResult(i, REQ_THIRD);
-
MainActivity에서 결과를 받았을 때 메소드 생성
// MainActivity.java @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if(requestCode == REQ_THIRD){ String resultStr = "Result Code: " + resultCode; if(data != null ){ String result = data.getStringExtra("result"); if(result != null) resultStr += (", " + result); } Toast.makeText(this, resultStr, Toast.LENGTH_SHORT) .show(); } }
-
SecondActivity에서 데이터 전송
// SecondActivity.java Intent i = new Intent(); i.putExtra("result", "OK"); setResult(Activity.RESULT_OK, i); finish();
-
-
문자 보내기 예시
Intent i = new Intent(Intent.ACTION_SENDTO); i.setData(Uri.parse("smsto:010-4524-5468")); i.putExtra("sms_body", "Hello"); startActivity(Intent.createChooser(i, "Select one"));
자세한 것은 생략
내 앱이 공유하기에 관심이 있다는 사실을 Android에게 알림
<!-- AndroidManifest.xml -->
<activity android:name=".ThirdActivity"
android:parentActivityName=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
그 뒤 ThirdActivity
에서 Intent를 받았을 때 행동 처리
// ThirdActivity.java onCreate메소드 안
Intent i = getIntent();
if(i.getType() != null && i.getType().equals("text/plain")){
setResult(Activity.RESULT_OK);
String text = i.getStringExtra(Intent.EXTRA_TEXT);
if(text != null){
TextView tv = findViewById(R.id.textView);
tv.setText(text);
}
}
key, value 형태의 간단한 데이터를 저장할 때 사용
-
Activity가 종료 된 이후에서 데이터를 저장할 수 있다.
-
정해진 위치에 파일이 생성되며 Key-value 형태로 저장된다.
-
Key값을 이용한 값의 저장 및 불러오기만 가능
// MainActivity.java
private SharedPreferences preferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
preferences = getSharedPreferences("user", MODE_PRIVATE);
String user = preferences.getString("user", null);
}
SharedPreferences.Editor editor = preferences.edit();
editor.putString("userName", userName);
// 아래 방법 중 하나를 선택
editor.apply(); // 비동기 방식
editor.commit(); // 동기 방식
안드로이드에서 기본 제공하는 DBMS
-
앱을 삭제할 경우에 데이터가 사라짐.
-
앱에서 DB를 만들고 버전 관리를 할 수 있다.
-
UserBean 클래스 생성
public class UserBean { private int sequenceNumber; private String name; public int getSequenceNumber() { return sequenceNumber; } public void setSequenceNumber(int sequenceNumber) { this.sequenceNumber = sequenceNumber; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
-
History Helper 클래스 생성
public class HistoryDBHelper extends SQLiteOpenHelper { // version을 제외한 다른 인자값에 @Nullable이 있다. 여기선 생략 public HistoryDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } }
-
History Helper 함수 구현
// HistoryDBHelper.java 클래스 내 @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { String sql = "create table history ( sequenceNumber integer primary key autoincrement, name text)"; sqLiteDatabase.execSQL(sql); }
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
String sql = "drop table history";
sqLiteDatabase.execSQL(sql);
onCreate(sqLiteDatabase);
}
public long insert(UserBean user){
SQLiteDatabase db = getWritableDatabase();
ContentValues value = new ContentValues();
value.put("name", user.getName());
return db.insert("history", null, value);
}
public ArrayList getAll(){
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query("history", null, null, null, null, null, null);
ArrayList result = new ArrayList<>();
while(cursor.moveToNext()){
UserBean user = new UserBean();
user.setSequenceNumber(
cursor.getInt(cursor.getColumnIndex("sequenceNumber"))
);
user.setName(
cursor.getString(cursor.getColumnIndex("name"))
);
result.add(user);
}
return result;
}
public long delete(UserBean bean){
SQLiteDatabase db = getWritableDatabase();
String sequence = String.valueOf(bean.getSequenceNumber());
return db.delete("history",
"sequenceNumber=?", new String[] {sequence});
}
public long delete(){
SQLiteDatabase db = getWritableDatabase();
return db.delete("history", null, null);
}
-
MainActivity 수정
private HistoryDBHelper dbHelper; @Override onCreate... { ... dbHelper = new HistoryDBHelper(this, "userdb", null, 1); }
-
유저 select 호출 및 출력
private void showUsers(){ ArrayList users = dbHelper.getAll(); for(UserBean u: users) Log.i("MAIN", "["+u.getSequenceNumber()+"]" + u.getName()); }
-
유저 추가
UserBean user = new UserBean(); user.setName(userName); dbHelper.insert(user);
나머지 스킵
각 어플리케이션은 고유의 ID와 영역을 가지고 실행된다.
-
내 영역 밖에 데이터에 마음대로 접근할 수 없음.
-
내 영역에 다른 Application도 들어올 수 없음
내 영역 밖에 데이터나 리소스가 필요하면 권한을 신청해야 함
-
Manifest.xml에 필요한 권한 추가 (정상 권한, 위험 권한 둘 다 해야함)
<uses-permission android:name="android.permission.INTERNET" />
-
정상 권한(다른 앱의 실행을 방해하거나 사생활을 침해하지 않는 동작)은 그냥 사용
-
위험 권한은 사용자의 허가를 얻어 사용
정상 권한의 예: 인터넷, 블루투스, 와이파이 상태 등
위험 권한: 캘린더, 통화 이력 등
정상 권한은 바로 API 사용이 가능하다.
- permission 요청
// MainActivity.java
public void onSendSMS(View v){
if( Build.VERSION.SDK_INT >= 23){
int permission = ContextCompat.checkSelfPermission(this,
Manifest.permission.SEND_SMS);
if(permission != PackageManager.PERMISSION_GRANTED){
requestPermissions(new String[] {
Manifest.permission.SEND_SMS}, REQ_SEND_SMS);
return;
}
}
sendSMS();
}
-
permission 결과 받기
// MainActivity.java @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if(requestCode != REQ_SEND_SMS) return; if(permissions[0].equals(Manifest.permission.SEND_SMS) && grantResults[0] == PackageManager.PERMISSION_GRANTED) sendSMS(); else Toast.makeText(this, "문자 전송 권한이 없습니다.", Toast.LENGTH_SHORT).show(); ]
배터리가 부족하다, 폰이 부팅되었다 등 시스템 이벤트를 수신하는 기능이다.
// MainActivity.java
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("Receiver", "onReceive");
int state =
intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
switch(state){
case BluetoothAdapter.STATE_ON:
Log.d("Receiver", "Bluetooth ON");
break;
case BluetoothAdapter.STATE_OFF:
Log.d("Receiver", "Bluetooth OFF");
break;
}
}
};
// MainActivity.java
@Override
protected void onStart(){
super.onStart();
IntentFilter filter = new
IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(receiver, filter);
}
@Override
protected void onStop(){
super.onStop();
unregisterReceiver(receiver);
}
-
Style: View에 적용하는 모양과 형식.
-
Theme: 액티비티나 Application에 적용하는 스타일.
res/values/style.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item
name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
res/values/style.xml에 커스텀 style 추가
<resources>
...
<style name="BigText" parent="Widget.AppCompat.Button">
<item name="android:textSize">24sp</item>
</style>
</resources>
사용
<Button
android:id="@+id/button"
style="@style/BigText"
...
widget은 여러 상태를 가진다.
위젯에 상태에 맞는 다향안 디자인을 적용하기 위해 필요한 것이 Selector이다.
-
colors.xml에 lightGray와 darkGray 추가 (스킵)
-
res폴더에서 layout file 생성. Root element는
selector
로 설정
- layout 파일 작성
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false"
android:drawable="@color/lightGray"></item>
<item android:state_pressed="true"
android:drawable="@color/colorAccent"></item>
</selector>
state_pressed로 버튼의 상태를 정의한다.
-
selector 작성
color폴더에 파일을 추가해야 한다.
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="false" android:color="@color/colorAccent"></item> <item android:state_pressed="true" android:color="@color/lightGray"></item> </selector>
-
위에서 만든 BigText를 수정
<style name="BigText" parent="Widget.AppCompat.Button"> <item name="android:textSize">24sp</item> <item name="android:padding">24dp</item> <item name="android:background">@drawable/button_background</item> <item name="android:textColor">@color/button_text_color</item> </style>
-
HTTP: 안드로이드 앱에서 서버로 HTTP Request를 보낼 수 있다.
서버에 응답 시간이 필요하기 때문에 Thread로 처리해야 한다.
안드로이드에서 메인 쓰레드에서만 UI의 업데이트를 허용하기 때문에 AsyncTask를 사용해야 한다.
AsyncTask란 별도의 Thread를 돌리면서도 UI를 갱신할 수 있도록 제공하는 클래스이다.
-
AndroidManifest.xml에 인터넷 권한 추가 (위에 참고)
-
Task 클래스 추가
class Task extends AsyncTask<URL, Integer, String>{ private WeakReference<MainActivity> activityReference; public Task(MaintActivity activity){ activityReference = new WeakReference<>(activity); } @Override protected void onProgressUpdate(Integer... values) {} @Override protected void onPreExecute() {super.onPreExecute();} @Override protected String doInBackground(URL... params) { return null; } @Override protected void onPostExecute(String s) { //super.onPostExecute(s); MainActivity activity = activityReference.get(); if (activity == null || activity.isFinishing()) return; } }
만약 서버가 고정IP를 가지지 못하는 경우 절대 localhost를 사용하면 안됨.
localhost로 요청하면 핸드폰 자기자신을 찾는다.
-
MainActivity에서
onCreate()
수정// MainActivity.java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = findViewById(R.id.textView); try { URL url = new URL("http://ip:port/context"); new Task(this).execute(url); }catch (MalformedURLException e) { } } @Override protected void onPostExecute(String s) { //super.onPostExecute(s); MainActivity activity = activityReference.get(); if (activity == null || activity.isFinishing()) return; textView.setText(s); }
-
Task 클래스에서 함수 구현
@Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); if(values.length > 0) Log.i("http", String.valueOf(values[0])); } @Override protected void onPreExecute() { super.onPreExecute(); } protected String doInBackground(URL... params) { int i=0; String result = new String(); if(params == null || params.length < 1) return null; try { publishProgress(i++); HttpURLConnection connection = (HttpURLConnection)params[0].openConnection(); connection.setRequestMethod("GET"); // get방식 //connection.setDoOutput(true); // 쓰기모드 - POST로 강제 설정됨 connection.setDoInput(true); // 읽기모드 connection.setUseCaches(false); connection.setDefaultUseCaches(false); publishProgress(i++); InputStream is = connection.getInputStream(); StringBuilder builder = new StringBuilder(); //문자열을 담기 위한 객체 BufferedReader reader = new BufferedReader(new InputStreamReader(is,"UTF-8")); //문자열 셋팅 String line; while ((line = reader.readLine()) != null) { builder.append(line+ "\n"); publishProgress(i++); } result = builder.toString();Log.i("http", "result=" + result); publishProgress(i++); } catch(IOException me){ me.printStackTrace(); } return result; }
Android의 Component중 하나로 화면 없이 동작하는 코드이다.
보통 음악 플레이어, 채팅(알림) 등에 사용된다.
AndroidManifest.xml에 기술되어야 한다.
-
startService() / stopService()
: service를 시작하거나 종료하는 메소드.메인 쓰레드 안에서 동작한다. (싱글톤으로 동작)
Activity와 Service는 독립적으로 동작하여서 Broadcast로 통신 가능
-
bindService() / unbineService()
: Service를 구동하는데, 모든 bind가 해제돼야 서비스가 종료된다. service와 차이점은 activity에서 service를 받아서 직접 데이터를 주고 받을 수 있다.
-
AndroidManifest.xml에 만든 서비스 추가
<service android:name=".SimpleService" android:enabled="true" android:exported="true"></service>
-
Service 생성
public void startService(View v){ Intent intent = new Intent(this, SimpleService.class); startService(intent); } public void stopService(View v){ Intent intent = new Intent(this, SimpleService.class); stopService(intent); }
생략
그림판을 만들어 본다.
-
Canvas: 그림을 그릴 대상, 그림판
-
Paint: 그림을 어덯게 그릴 것인가에 대한 정보를 가짐. 색, 선, 굵기
-
onDraw(): 위젯을 새로 그려야 할 시점에 호출됨
파라미터에서 Canvas가 전달되며 그림을 그리면 화면에 출력된다.
-
invaildate(): 개발자가 정한 시점에 화면을 갱신해야 할 경우 onDraw()를 호출하는 것이 아니라 invalidate()를 호출한다. 그러면 필요한 정보들이 준비된 다음 onDraw()가 호출된다.
-
전체 화면 앱으로 설정
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
-
DrawingView 클래스 생성 (선 색 변경 하지 않은 상태)
public class DrawingView extends AppCompatImageView implements View.OnTouchListener { private Path path; public DrawingView(Context context) { super(context); init(); setOnTouchListener(this); } public DrawingView(Context context, AttributeSet attrs) { super(context, attrs); } public DrawingView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private void init(){ paint = new Paint(); paint.setColor(Color.BLACK); paint.setStyle(Paint.Style.STROKE); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStrokeWidth(10); paint.setAntiAlias(true); path = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawPath(path, paint); } @Override public boolean onTouch(View view, MotionEvent motionEvent) { int action = motionEvent.getAction(); switch(action){ case MotionEvent.ACTION_DOWN: path.lineTo(motionEvent.getX(), motionEvent.getY()); break; case MotionEvent.ACTION_MOVE: path.moveTo(motionEvent.getX(), motionEvent.getY()); break; case MotionEvent.ACTION_UP: break; } invalidate(); return false; } }
-
레이아웃(activity_main)에 DrawingView 추가
<com.andrstudy.drawingview.DrawingView android:id="@+id/drawingView" android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true" <!-- 이부분 중요 --> app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/>