Android 백과사전 1탄

 

액티비티 생명주기

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        
        super.onCreate(savedInstanceState)  // 현재 UI를 일단 저장해둠
        setContentView(R.layout.activity_main)  // R.layout.activity_main이 View 타입 반환
    }
}

  • onCreate()

    • 데이터를 목록에 바인딩, ViewModel과 활동 연결등 화면 초기구성 관련 코드를 수행
    • 이후 onStart()와 onResume()를 연달아서 호출함
    • Activity 처음 생성될 때 실행됨
  • onStart()

    • Activity를 포그라운드에 보내 상호작용할 수 있도록 준비함. 예를들어 이 메서드에서 앱이 UI를 관리하는 코드를 초기화함
    • Activity가 보여지기 직전에 호출됨
  • onPause()

    • 사용자가 Activity를 떠나면 이 메서드를 호출함. Activity가 포그라운드에 있지 않게 됐다는걸 의미함. (사용자가 멀티윈도우 모드면 여전히 표시가 가능)
    • 상태바를 내리거나 긴급메세지가 팝업되거나 하는등 다른 Activity가 화면에 나타나는 시점에 호출됨
  • onResume()

    • Activity가 포그라운드에 표시됨. 여기서 어떤 이벤트가 발생해서 포커스가 변경될 때 까지 앱이 onResume()상태에 머무름.
    • 전화가 오거나, Activity가 전환되거나 했을 경우 onResume()을 벗어남
    • Activity가 보여지고 Input을 받을 수 있는 상태에서 호출됨
  • onRestart()

    • onPause()가 호출되었던 Activity를 복원시킬 때 호출됨
  • onStop()

    • 상태바, 긴급메세지 등 일시정지인 상태가 아니라 뒤로가기로 App이 더이상 화면에 나오지 않게 했을 때 호출됨
      • App 뒤로가기 한 다음에 멀티 윈도우 버튼 누르면 App이 여전히 있는것을 확인할 수 있음
  • onDestory()

    • 멀티 윈도우에 있던 App이 오랜시간이 지나면 사용자가 App을 안쓴다고 판단해서 핸드폰이 자체적으로 대기중인 App을 종료시켜버림. 그때 호출됨

네이밍 규칙

  • 액티비티에 대한 모든 컴포넌트는 액티비티 이름으로 시작해야 함
activity_login_btn_login
activity_login_et_username
activity_login_et_password
  • 클래스, 메소드명은 파스칼 표기법(여러 단어를 붙여서 한 단어로 표기할 때 각 단어의 첫 문자를 대문자로 하는 것)
public class HelloWorld{
    public void HelloCity(){

    }
}
  • 변수, 파라미터 등은 카멜 표기법
int totalCost = 0;

String fullName = "";

public void HelloCity(String familyName){};

안드로이드 4가지 구성요소

  • Activity
  • Service
  • Broadcast Receiver
  • Content Provider

Intent

Intent는 위 4가지 구성요소(component)간에 작업 수행을 위한 정보 전달 역할

  • 명시적 인텐트
    • 인텐트에 클래스 객체나 컴포넌트 이름을 지정하여 호출될 대상을 명확히 알 수 있는 경우
val intent = Intent(this, SubActivity::class.java)
startActivity(intent)
  • 암시적 인텐트
    • 호출된 작업의 속성만 지정해 두었으며 호출될 대상이 달라질 수 있는 경우
    • 인텐트 필터 개념은 암시적 인텐트에만 쓰임 (해석하는데 필요)
val intent = Intent(Intent.ACTION_DIAL)
val TEST_DIAL_NUMBER = Uri.fromParts("tel", "5551212", null)
intent.data = TEST_DIAL_NUMBER
startActivity(intent)

  • 위 그림과 같이 Intent action은 String 타입임

Intent-filter

  • 구성요소는 3가지
    1. <action>
      • name 속성에서 허용된 intent 작업 선언. 문자열이어야 함
    2. <data>
      • 허용된 데이터 유형 선언. 하나 이상의 속성을 사용하여 데이터 URI, MIME 유형 나타냄
    3. <category>
      • name 속성에서 허용된 인텐트 카테고리 선언. 문자열이어야 함
  • 아래 예시를 통한 이해가 훨씬 빠를
<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="ShareActivity">
    <!-- 이 액티비티는 텍스트데이터와 함께 SEND 액션을 수행합니다 -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
    <!-- 이 액티비티는 미디어데이터와 함께 SEND와 SEND_MULTIPLE을 수행합니다-->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <action android:name="android.intent.action.SEND_MULTIPLE"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="application/vnd.google.panorama360+jpg"/>
        <data android:mimeType="image/*"/>
        <data android:mimeType="video/*"/>
    </intent-filter>
</activity>
  • MainActivity
    • ACTION_MAIN 작업은 이곳이 앱 주요 진입 지점이며 사용자가 어플 아이콘을 클릭해서 처음 시작할 때 열리는 액티비티임을 명시하는것
    • CATEGORY_LAUNCHER 카테고리는 이 액티비티 아이콘이 시스템의 앱 시작 관리자에 배치되어야 한다는것을 의미
  • ShreActivity는 텍스트, 미디어 콘텐츠 공유를 용이하게 할 목적임
    • MainActivity에서 직접 진입할수 있지만 전혀 다른 앱에서 두가지 인텐트 필터 중 하나와 일치하는 암시적 인텐트를 발생시키는 전혀 다른 앱에서 SharedActivity에 직접 진입할 수 있음

Content-Provider

데이터 제공하는 역할을 하며 여러 App들이 서로 데이터를 공유하는 유일한 방법

File-Provider

위 Content-Provider를 상속하며 안전한 파일 공유가 가능하게함

  • file:// 형태의 uri -> content:// 형태의 uri로 변경
    • content URI는 임시 권한 부여가 가능
  • 사용법
manifest.xml 파일

<manifest> 
    ... 
    <application> 
        ... 
        <provider 
            android:name="androidx.core.content.FileProvider"   // 이건 고정
            android:authorities="com.mydomain.provider"     // 도메인이름.fileprovider 혹은 도메인이름.provider
            android:exported="false"                            // 다른 app에서 접근못하게 하도록 false
            android:grantUriPermissions="true">                 // 임시 권한 부여를 위해 해당속성을 true로 변경
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"      // 고정
                android:resource="@xml/file_paths">                     // 정의한 xml 파일을 연결(이 xml 파일에 경로 명시함)
        </provider> 
    ... 
    </application>
</manifest>
xml/file_paths.xml 파일

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/" />          // Context.getFilesDir()에 해당하는 영역
                                                            // name 속성은 실제 path를 숨기기 위한 일종의 별명
                                                            // path 속성의 값이 실제 경로임. 파일이 아니라 디렉토리를 넣어줘야 한다는것 주의
                                                            // 조금더 해석하자면 실제 경로는 Context.getFilesDir()/images/ 에 해당하는 모든 파일
                                                            // contentURI는 content://com.mydomain.fileprovider/my_images/default_image.jpg로 나옴
    <files-path name="my_docs" path="docs/"/>
</paths>
File imagePath = new File(Context.getfilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.provider", newFile);
  • paths 태그 들어갈 수 있는 것
    • files-path
      • 여러개 들어갈 수 있음
      • Context.getFilesDir()에 해당하는 영역
    • cache-path
      • Context.getCacheDir()에 해당하는 영역
    • external-files-path
      • Context.getExternalFilesDir(String)에 해당하는 영역
    • external-cache-path
      • Context.getExternalCacheDir()에 해당하는 영역
    • external-media-path
      • Context.getExternalMediaDirs()에 해당하는 영역

레이아웃 인플레이터

XML에 미리 정의해둔 틀을 실제 메모리에 올려주는 역할. 즉 LayoutInflater는 XML에 정의된 Resource를 View 객체로 반환해주는 역할

setContentView(R.layout.activity_main)      // 이것도 역시 Inflater 역할
FrameLayout container = (FrameLayout) findViewById(R.id.container);
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.sub1, container, true);       // R.layout.sub1: 객체화 하고픈 xml 파일
                                                        // 객체화한 View를 넣을 부모 레이아웃
                                                        // 바로 인플레이션 할지 여부

// 위 코드를 통해 사전에 미리 선언한 container 레이아웃에 작성한 xml 메모리 객체가 삽입된다.

뷰 바인딩

  • findViewById로 일일이 xml과 매칭시키는것은 상당히 귀찮음. xml 파일의 id를 바로 kotlin class에서 사용할 수 있도록 해줌

Activity 에서 뷰 바인딩

android {
        .
        .
        .
    buildFeatures {
        viewBinding = true
    }
}
// 뷰바인딩 적용
class RecyclerActivity : AppCompatActivity() {
    private lateinit var binding: ActivityRecyclerBinding       // Activity 이름에 걸맞게 타입을 적어줘야 함

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityRecyclerBinding.inflate(layoutInflater)    // 여기도 마찬가지
        setContentView(binding.root)        // 여기도 변경

    }
}

Fragment 에서 뷰 바인딩

 private var _binding: ResultProfileBinding? = null
    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = ResultProfileBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

Activity 내에서 Fragment 보여줘야 하는 상황일 때 뷰 바인딩

  • ChangeFragmentActivity.kt
package com.example.android_study

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import com.example.android_study.databinding.ActivityChangeFragmentBinding
import com.example.android_study.databinding.FragmentMyBinding

class ChangeFragmentActivity : AppCompatActivity() {

    private lateinit var binding : ActivityChangeFragmentBinding
    private lateinit var myFragment: MyFragment
    private lateinit var mySecondFragment: MySecondFragment

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityChangeFragmentBinding.inflate(layoutInflater)

        setContentView(binding.root)

        binding.fragmentChangeBtn1.setOnClickListener {
            myFragment = MyFragment.newInstance("hohohoho", "dfsfs")
            supportFragmentManager.beginTransaction().replace(R.id.activity_change_frame_layout, myFragment).commit()
        }

        binding.fragmentChangeBtn2.setOnClickListener {
            mySecondFragment = MySecondFragment.newInstance("hohohohoasfslkadjf", "Asdfsa")
            supportFragmentManager.beginTransaction().replace(R.id.activity_change_frame_layout, mySecondFragment).commit()
        }
    }
}
  • MyFragment.kt
package com.example.android_study

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.android_study.databinding.FragmentMyBinding

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
 * A simple [Fragment] subclass.
 * Use the [MyFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class MyFragment : Fragment() {
    // TODO: Rename and change types of parameters
    private var param1: String? = null
    private var param2: String? = null


    private var _binding: FragmentMyBinding? = null
    private val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        _binding = FragmentMyBinding.inflate(inflater, container, false)
        return binding.root       ?
    }

    override fun onDestroyView() {
        _binding = null     // Fragment가 없어질 때 _binding을 null 처리함
        super.onDestroyView()
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.textview.text = param1

//        super.onViewCreated(view, savedInstanceState)
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment MyFragment.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            MyFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

해상도 단위

  • dp, dpi (px대신 이것 사용)
    • 여러 단말기 화면에서 앱의 전체 비율을 유지하기 위함
  • sp (텍스트에서는 이것 사용)
    • 단말기 자체의 글자 크기(크게, 보통, 작게)를 반영하는 단위

레이아웃

  • 위 그림에서 weight를 3, 2를 줬는데 버튼 2까지가 화면상 가운데까지 차지하는게 아니라 넘어버린다. wrap_content는 버튼의 최소 크기를 가지고 있기 때문이다. 따라서 0dp로 만들어줘야 한다

버튼

이미지 넣기

  • 커스텀 버튼을 사용하고 싶은 경우 android.widget.Button을 사용한다
    • 그냥 Button은 몇가지 속성을 무시해서 변경해도 적용이 안된다
<android.widget.Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/custom_youtube_button"
        android:drawableLeft="@drawable/youtube_button"
        android:text="Button"/>

  • drawable 폴더에 이미지 넣기

  • drawable(우클릭) -> New -> Resource File 생성해서 xml 파일 이름 지정(같은 파일 이름이면 안됨)

  • layer list, item 태그 활용

xml로 모양 수정

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
    <solid android:color="#31D1BF"/>
    <corners android:radius="50dp" />
    <size android:height="70dp" android:width="200dp"/>
    <stroke android:width="3dp" android:color="#A232D3"/>
    <gradient
        android:startColor="#0BFBB9"
        android:centerColor="#4417EA"
        android:endColor="#A32E3B"
        android:gradientRadius="300dp"
        android:centerX="1"
        android:centerY="1"
        android:type="radial"
</shape>

  • 원을 중심으로 gradient를 줄 것이며 300dp 만큼 주겠다. 이때 원의 포지션은 (1, 1) 이다
    • 왼쪽 위 (0, 0), 오른쪽 아래 (1, 1)

버튼 눌렀을 때 모양 변경되게

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
    <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
        <solid android:color="#31D1BF"/>
        <corners android:radius="50dp" />
        <size android:height="70dp" android:width="200dp"/>
        <stroke android:width="3dp" android:color="#A232D3"/>
        <gradient
            android:startColor="#0BFBB9"
            android:centerColor="#4417EA"
            android:endColor="#A32E3B"
            android:gradientRadius="300dp"
            android:centerX="1"
            android:centerY="1"
            android:type="radial"/>
    </shape>
</item>

<item android:state_pressed="false">
    <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
        <solid android:color="#31D1BF"/>
        <corners android:radius="50dp" />
        <size android:height="70dp" android:width="200dp"/>
        <stroke android:width="3dp" android:color="#A232D3"/>
    </shape>
</item>
</selector>
  • item 태그로 이벤트 설정한 뒤 그 안에서 shape 지정

EditText 키보드 이벤트 처리 (키보드 숨기기)

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val et_id = findViewById<EditText>(R.id.et_id)
        val et_pw = findViewById<EditText>(R.id.et_pw)

        // 키보드의 엔터가 눌렸을 때 이벤트 처리하기 위한 코드
        et_pw.setOnEditorActionListener() { v, actionId, event ->
            if(actionId == EditorInfo.IME_ACTION_DONE){// 엔터가 눌렸다면
                Login(v) // 아래 정의한 함수를 호출하라
                true     // 반환 타입은 boolean
            } else {
                false   // 반환 타입은 boolean
            }
        }
    }

    fun Login(v: View){

        // 키보드 숨기기
        var imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(v.windowToken, 0)

        Log.d(TAG, "Login: 로그인 성공")
        Toast.makeText(this, "로그인 성공", Toast.LENGTH_LONG).show()
    }
}

EditText 로그인 처리 및 불러오기

class MainActivity : AppCompatActivity() {
    lateinit var et_id:EditText
    lateinit var et_pw:EditText

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        et_id = findViewById<EditText>(R.id.et_id)
        et_pw = findViewById<EditText>(R.id.et_pw)

        // 아이디, 비밀번호가 저장되어 있다면 불러오고 그렇지 않으면 빈칸으로 세팅함
        // onCreate에서 이렇게 구현하면 한번만 로그인에 성공했던 디바이스의 경우 매번 로그인정보가 불러와지는 효과를 가진다
        var pref = this.getPreferences(0)
        et_id.setText(pref.getString("아이디", ""))
        et_pw.setText(pref.getString("비밀번호", ""))

        et_pw.setOnEditorActionListener() { v, actionId, event ->
            if(actionId == EditorInfo.IME_ACTION_DONE){
                Login(v)
                true
            } else {
                false
            }
        }
    }

    fun Login(v: View){
        if(et_id.text.toString() == "hojun" && et_pw.text.toString() =="0303"){
            Toast.makeText(this, "로그인 성공", Toast.LENGTH_LONG).show()
            var editor = this.getPreferences(0).edit()
            editor.putString("아이디", "hojun").apply()
            editor.putString("비밀번호", "0303").apply()
        }

        else{
            Toast.makeText(this, "로그인 실패", Toast.LENGTH_LONG).show()
        }

        // 키보드 숨기기
        var imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(v.windowToken, 0)
    }


}

비디오뷰

  • raw 파일 추가 및 mp4 파일 추가

  • activity_video.xml ```xml <?xml version=”1.0” encoding=”utf-8”?>

- VideoActivity.kt

```kotlin
package com.example.android_study

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.MediaController
import com.example.android_study.databinding.ActivityVideoBinding

class VideoActivity : AppCompatActivity() {

    private lateinit var binding: ActivityVideoBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityVideoBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val videoPath = "android.resource://" + packageName + "/" + R.raw.lol
        binding.activityVideoVideoView.setVideoPath(videoPath)

        val mediaController = MediaController(this)
        binding.activityVideoVideoView.setMediaController(mediaController)
        mediaController.setAnchorView(binding.activityVideoVideoView)
        binding.activityVideoVideoView.keepScreenOn = true
    }
}