앱 프로젝트 - 04 - 1 (계산기) - Layout( TableLayout, ConstraintLayout ), LayoutInflator, Room( LocalDB기능 ), LocalDB에서의 Thread 사용 + runOnUiThread, Span( Text에 효과주기 ), ripple( 해당 Shape Drawable 클릭 시 색 변경 ), 확장함수 만들기, Vector asset에서 내장되어 있는 이미지 생성해서 가져오기

하이루·2022년 1월 14일
0
post-thumbnail

소개

계산기 앱


레이아웃 소개

계산 기록이 저장됨


시작하기에 앞서 알고갈 것들

LayoutInflater

Layout을 연결할 때 일반적으로 setOnContentView라는 메소드를 사용했었는데, 이렇게 통 xml파일을 연결하는 것이 아닌
xml을 View의 형태로 작게 사용할 수 있는데, 이때 이렇게 쪼갠 xml을 메모리에 올려줄 수 있게 해주는 기능이 LayoutInflater

즉, 반복적으로 사용되는 xml을 ( xml파일 통으로 한꺼번에 올리는 것이 아니라 ) View단위로 쪼개서 사용을 해보고, LayoutInflator을 통해서 메모리에 불러와서 할당하는 것이다.

일반적인 사용목적

일반적으로 데이터 수에 따라 View의 개수가 달라지거나 (이 경우 반복문과 함께 쓰임),
복잡한 View를 커스텀 한 뒤 넣고 싶을 경우 등등에 사용됨

이외에도 필요에 따라 사용하면 됨

LayoutInflater 사용법

  1. LayoutInflater로 불러올 xml파일을 구성함
  • xml 파일을 만들 때 해당 xml이 뷰로 들어갈 때를 고려해서ㅡ 가장 최상위 Layout의 width와 height를 선정해야한다.
    ( 일반적으로 width는 match_parent로 height는 wrap_content로 한다. )
  1. LayoutInflater를 이용하여 해당 xml파일을 가져옴

  2. 가져온 xml파일로 메인 액티비티에 넣는 식으로 사용

LayoutInflater 예시 - 1 LayoutInflater를 통해 불러올 xml파일을 만듬

--> 위의 xml파일을 LayoutInflater를 이용해서 불러올 것임

LayoutInflater 예시 - 2, 3 LayoutInflater를 이용하여 해당 xml파일을 불러옴 + 액티비티에 삽입


......


 private val historyLinearLayout: LinearLayout by lazy {
        findViewById( R.id.historyLinearLayout)
    }

......

  val historyView = LayoutInflater.from(this).inflate(R.layout.history_raw,null,false)
                    historyView.findViewById<TextView>(R.id.expressionTextView).text = "aaaaa"
                    historyView.findViewById<TextView>(R.id.resultTextView).text = "= "bbbbbb"

                    historyLinearLayout.addView(historyView)

......

LayoutInflater의 from()메소드 현재위치를 파라미터로 받으며,
반환값에 대해 inflate()를 확장함수처럼 사용하고 있다.

inflater()
첫번째 파라미터로 불러올 xml파일의 주소값을 받으며,
두번째 파라미터로 root를
세번째 파라미터로 attachToRoot를 받는다.
(뒤의 두개는 아직 사용하지 않으므로 null, false로 해주었다.)

이후 inflater를 통해 가져온 xml 파일을 historyView라는 상수를 생성하여 할당해주었고,
해당 xml파일 내부의 View에 접근할 때는 historyView상수에서 findViewById메소드를 실행시켜서 접근할 수 있다.

마지막으로 그렇게 불러온 xml파일을 addView()메소드를 사용하여 메인 액티비티의 레이아웃 안에 View로써 넣어주고 있다.


Room

공식문서 : https://developer.android.com/training/data-storage/room?hl=ko

localDB에 저장하도록 도와주는 기능

  • DB에 저장하는 기능을 하게되면 Thread를 필수적으로 사용해야 된다.

Room 사용법

1. gragle를 통해 Room 라이브러리를 받는다.

1-1. bulid.gradle 중에 .app이 붙어있는 gradle로 이동

1-2. 해당 파일의 plugin에 id 'kotlin-kapt' 추가

1-3. 해당 파일의 dependencies 에 다음의 코드추가

// plugins에 추가
    id 'kotlin-kapt'


// dependencies에 추가
kapt "androidx.room:room-compiler:2.2.6"
implementation "androidx.room:room-runtime:2.2.6"

// 여기서 2.2.6은 현재 최신버전의 Room이므로 공식문서에서 최신버전 확인할 것

1-4. sync 클릭

이런 방법들은 기본적으로 공식문서에 Room에 대한 항목을 보면 Room 추가하기에 나와있음

2. 데이터베이스에서 데이터를 담기 위한 데이터 모델을 따로 생성 -> @Entity ( 데이터 모델 = 데이터를 담을 클래스 )

  • 데이터 모델을 위한 파일을 따로 생성
  • @Entity선언을 해주고
  • data class로 데이터를 담기위한 클래스를 만들어주고
  • 변수에 @PrimaryKey를 선언하여 PrimaryKey로 설정 ( 해당 데이터는 null이 들어가도 자동으로 하나씩 숫자가 올라감 )
  • 변수에 @ColumnInfo(name="속성명")을 선언하여 속성을 만들어줌
2-1. ex) 데이터 파일
package fastcampus.aop.part1.aop_part2_chapter4.model

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey


@Entity
data class History(
    @PrimaryKey val uid: Int?,
    @ColumnInfo(name="expression") val expression: String?,
    @ColumnInfo(name = "result") val result: String?


)

2-2. 해당 코드 설명
--> 해당 파일은 DB에 넣기 위한 MODEL을 만들기 위한 파일임 ( ModelClass >> 데이터를 담기위한 Class )

--> @Entity를 해줬으므로 History 는 Room의 데이터클래스로 변환되었음
이후 Room의 사용 방식에 따라 코드의 내용과 같이
각 데이터상수에
@PrimaryKey,
@ColumnInfo(name="속성명")
을 붙여주는 것을 통해 Room을 이용하여 DB의 데이터 구조를 만들 수 있다.

또한 data class로 만들었기 떄문에
getter, toString(), equals(), hashcode, copy가 자동으로 생성됨, 각 변수가 val이므로 setter는 생성안됨 ( 수정이 불가하기 때문 )

또한 이후 DB를 만들 당시에 이 구조를 바탕으로 하기 때문에 DB의 구조또한 여기서 결정난다고 볼 수 있다.

3. 데이터베이스를 제어하기 위한 메소드들을 따로 모아서 인터페이스로 생성 -> @Dao

  • DB에서 데이터를 주고 받기 위한 메소드들을 모아놓기 위한 파일 따로 생성
  • @Dao를 선언
  • interface로 데이터를 제어하기 위한 인터페이스를 만들어줌
  • @Query, @Insert, @Delete등을 이용하여 데이터를 제어하는 메소드들을 선언해줌

3-1. ex) 데이터 파일

package fastcampus.aop.part1.aop_part2_chapter4.dao

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import fastcampus.aop.part1.aop_part2_chapter4.model.History



@Dao
interface HistoryDao {

    @Query("SELECT * FROM history")
    fun getAll(): List<History>
    // 해당 함수를 사용하면 위에 설정한 Query를 통해 데이터를 가져오게 됨
    //@Dao선언을 했기 때문에 Room의 데이터 인터페이스가 된것이고, 그래서 위와 같이 @Query를 사용할 수 있는 것임

    @Insert
    fun insertHistory(history: History)
    // History데이터 클래스로 데이터를 받아서 넣는다. ( @Insert )

    @Query("DELETE FROM history")
    fun deleteAll()
    // @Query를 이용 ->  모든 데이터 삭제

    @Delete
    fun delete(history: History)
    //History데이터 클래스로 데이터를 받아 특정 데이터만을 삭제

    @Query("SELECT * FROM history WHERE result LIKE :result")
    fun findByResult(result: String): List<History>
    // result값을 파라미터로 받아 Query에 넣어 해당 데이터만을 가져오게 만듬
    // ":파라미터명" 을 통해 Query에서 파라미터에 접근할 수 있음
    
    @Query("SELECT * FROM history WHERE result LIKE :result LIMIT 1")
    fun findByResultOnlyOne(result: String): History
    // 위와 같지만 하나의 데이터만 반환됨

}

3-2. 해당 코드 설명
--> @Dao란 Room에서 DB에 데이터를 주고받을 수 있게 해주는 기능을 말한다.

--> 보면 알겠지만, 데이터를 주고받는 단위는 2번에서 정의한 데이터 모델이다.

@Insert --> 아래의 메소드 실행시 받은 파라미터를 DB에 넣음 ( 위에서 만든 데이터 모델이 파라미터로 들어옴 )
@Delete --> 아래의 메소드 실행시 받은 파라미터의 데이터를 DB에서 제거 ( 위에서 만든 데이터 모델이 파라미터로 들어옴 )
@Query("쿼리내용") --> 아래의 메소드 실행시 해당 쿼리 실행
--> @Query의 경우ㅡ, 쿼리를 통해 DB를 제어하거나 데이터를 가져오거나 할 수도 있다.

아래의 코드와 같이
result값을 파라미터로 받아 Query에 넣어 해당 데이터만을 가져오게 만듬
--> ":파라미터명" 을 통해 Query에서 파라미터에 접근할 수 있음

     @Query("SELECT * FROM history WHERE result LIKE :result")
    fun findByResult(result: String): List<History>

4. DB를 생성하기 위한 추상 클래스를 따로 생성 -> @Database

  • DB를 생성하기 위한 파일을 따로 생성
  • @Database를 선언
  • @Database에서 entities의 파라미터로 2번에서 만든 데이터모델을 리스트형태로 넣음
  • @Database에서 version의 파리미터로 지정한 버전번호를 넣음
  • RoomDatabase()를 상속받는 추상 클래스를 생성
  • 추상 메소드로 3번에서 만든 인터페이스 파일( @Dao )을 구현함 ( 해당 인터페이스를 반환 )

4-1. ex) 데이터 파일

package fastcampus.aop.part1.aop_part2_chapter4

import androidx.room.Database
import androidx.room.RoomDatabase
import fastcampus.aop.part1.aop_part2_chapter4.dao.HistoryDao
import fastcampus.aop.part1.aop_part2_chapter4.model.History



@Database(entities = [History::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    
    abstract fun historyDao(): HistoryDao
    //HistoryDao는 내가 만든 Interface로 데이터베이스에 데이터를 넣거나 가져오거나 삭제하거나 등등의 메소드들이 들어있다. ( Room의 기능인 Dao기능 )

}

4-2. 해당 코드 설명

--> Room의 기능으로 데이터베이스를 만드는 파일

@Database( entities=[데이터구조] , version=버전번호 )를 통해 명시
entities에 통해 데이터베이스에서 사용할 Table의 구조를 리스트형식으로 넣어주고, version에 해당 Database의 버전을 명시

version을 명시하는 이유는 필요에 따라 DB의 Column구조가 변화할 경우ㅡ, 버전 1에서 버전 2로 가는 식으로 마이그레이션을 통해서 이동해줘야 하기 떄문에 현재 version을 명시하는 것

해당 클래스는 추상클래스이며, RoomDatabase()를 상속

HistoryDao는 3번에서 내가 만든 Interface로 데이터베이스에 데이터를 넣거나 가져오거나 삭제하거나 등등의 메소드들이 들어있다.
일반적으로 이것이 인터페이스의 역할이다.( 추상클래스가 상속의 설계도에 더 가깝다면, 인터페이스는 이와 같이 사용의 설계도( 메소드의 설계도 )와 더 가깝다고 볼 수 있다. )

  1. 메인 액티비티에서 구성한 Room을 사용하여 localDB이용
  • 4번에서 만든 추상클래스 타입의 변수를 선언

......

    lateinit var db: AppDatabase
    // Room을 이용하기 위해 AppDatabase를 담을 변수 생성
    
        override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

......



        db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "historyDB"
            ).build()

        
        
 ......       
 
 
 
              Thread(Runnable {
            db.historyDao().insertHistory(History(null, expressionText, resultText))
            // PrimaryKey로 선언한 파라미터는  null로 넣어도 자동으로 하나씩 값이 올라간다.
            
            
        }).start()   
        
        
        
         ......
        
        
              Thread(Runnable {
            db.historyDao().getAll().reversed().forEach{
                

                runOnUiThread{
                // 이와같이 액티비티에 직접 영향을 줘야하는 경우 runOnUiThread를 사용하여 UI쓰래드에서 처리하도록 해야함
                    

                    val historyView = LayoutInflater.from(this).inflate(R.layout.history_raw,null,false)
                    historyView.findViewById<TextView>(R.id.expressionTextView).text = it.expression
                    historyView.findViewById<TextView>(R.id.resultTextView).text = "= ${it.result}"

                    historyLinearLayout.addView(historyView)

                }

            }

        }).start()
       

......

5-1 코드 설명

4번에서 만든 추상클래스를 담는 변수 선언

databaseBuilder()를 이용해여 데이터베이스 생성 --> 첫번째 파라미터로 해당 앱의 위치 , 두번째 파라미터로 room의 @Database를 선언한 추상클래스, 세번째 파라미터로 생성할 DB의 이름을 넣어준다.
--> 이후 마지막에 build()메소드를 통해 해당 데이터베이스를 생성하여 변수에 넣어준다.

이후 생성한 DB를 이용하여 데이터베이스를 사용할 수 있는데,
DB에서 데이터를 넣고 빼는 등의 작업들은 UI쓰래드에서 처리하면 안되고, 위와 같이 따로 쓰래드를 만들어서 처리해줘야 함
-> 앱의 처리효율문제 + DB제어 때문에 앱이 멈추지 않도록

그리고 쓰래드에 넣어서 DB를 처리하는 할때 필요에 따라 DB로 처리한 데이터에 따라 앱에 직접적인 영향을 줘야하는 경우
runOnUiThread를 사용하여 새로운 쓰래드에서 UI쓰래드로 데이터를 전달하여 처리하도록 해야한다.
( 새로운 쓰래드는 앱에 직접적인 영향을 줄 수 없음 --> UI쓰래드를 사용해야함 )

runOnUiThread를 통해 UI쓰래드를 열어줌 -> 새로운 쓰래드로는 앱의 내용에 영향을 줄 수 없음 -> 새로운 쓰래드에서 UI쓰래드로 데이터를 넘겨줘야함
runOnUiThread를 구현하는 코드 안에는 Handler가 있음 -> Handler는 쓰래드끼리 데이터를 전달할 수 있게 해줌 -> 이를 통해 새로운 쓰래드에서 UI쓰래드로 데이터를 전달하여 UI쓰래드에서 처리하도록 하는 것

DB 새로 구성할 때 버전업 하는 법

  • 위에서 Room의 DB를 생성하는 추상 클래스를 보면 알겠지만,
    Room에는 Version이라는 개념이 존재한다.

    Version은 말그대로 해당 DB의 Version을 의미하며,

    DB의 구조가 변경될 경우ㅡ,
    Version을 올려서 새로 배포할 필요가 있다.

  • 여기서 중요한 점이 DB는 현재 구조적으로 데이터들을 담고 있으며,
    따라서 그냥 내가 코드를 바꿔서 DB를 새로 구성했다고해서 그에 맞춰 갈 수는 없다.

    • 데이터가 어떻게 이동하고, 무엇이 새로 생기고 이런 DB의 사용 부분에 대해 안드로이드 스튜디오는 모르기 때문이다.
  • 따라서 이렇게 DB를 고치는 부분에 대해 문제가 없도록,
    무결성을 가진 SQL의 언어로 DB가 어떻게 변화될지를
    안드로이드 스튜디오에 명령해 줄 필요성이 있는데,
    그것이 Migration이다.

Migration의 예시를 들자면 다음과 같다.

  • Version = 1 인 AppDatabase.kt파일 --> 처음 구성상태

    @Database(entities = [History::class], version = 1)
    abstract class AppDatabase : RoomDatabase() {
       abstract fun historyDao(): HistoryDao
    }
    
    fun getAppDatabase(context: Context): AppDatabase {
       return Room.databaseBuilder(
           context,
           AppDatabase::class.java,
           "BookSearchDB"
       ).build()
    }
  • Version = 2 인 AppDatabase.kt파일 --> 처음 구성상태에서 DB에 변화가 생겨,
    DB의 구성을 바꾸고 이를 Migration으로 명시해줌

     // 만약 Database를 업그레이드 하여 다음 버전으로 갈 경우, migration을 통해 변경사항을 알려줘야한다.
     @Database(entities = [History::class, Review::class], version = 2)
     abstract class AppDatabase : RoomDatabase() {
         abstract fun historyDao(): HistoryDao
         abstract fun reviewDao(): ReviewDao
     }
    
     fun getAppDatabase(context: Context): AppDatabase {
     
         // 1버전에서 2버전으로 가는 Migration을 구현해준 것
         // 버전을 올릴 떄 이렇게 Migration을 반드시 구현해줘야 함
         val migration_1_2 = object :Migration(1,2){
             override fun migrate(database: SupportSQLiteDatabase) {
                 //TODO 버전이 올라갈때ㅡ, DB에 어떤 변경사항이 있을지 직접 Query문으로 작성해줘야 함
                 database.execSQL("CREATE TABLE `REVIEW` (`id` INTEGER, `review` TEXT" + "PRIMARY KEY(`id`))")
    
             }
         }
    
         // 가져온 DB를 build()하기 전에, addMigrations() 메소드를 사용하여 Migration 세팅
         return Room.databaseBuilder(
             context,
             AppDatabase::class.java,
             "BookSearchDB"
         )
             .addMigrations(migration_1_2)
             .build()
     }
    • 이 부분에서 기존의 DB에서 Review라는 새로운 DB가 나타났기 때문에
      이것을 적용시켜서 Version = 2 로 업그레이드 시켜줌

      @Database(entities = [History::class, Review::class], version = 2)
      abstract class AppDatabase : RoomDatabase() {
        abstract fun historyDao(): HistoryDao
        abstract fun reviewDao(): ReviewDao
      }
    • 이 부분에서 Version UP에 대한 변경사항을 제공하기 위한 Migration을 구현하고 있음

           val migration_1_2 = object :Migration(1,2){
             override fun migrate(database: SupportSQLiteDatabase) {
                 //TODO 버전이 올라갈때ㅡ, DB에 어떤 변경사항이 있을지 직접 Query문으로 작성해줘야 함
                 database.execSQL("CREATE TABLE `REVIEW` (`id` INTEGER, `review` TEXT" + "PRIMARY KEY(`id`))")
      
             }
         }
      • Migration() 추상 클래스를 무명클래스로 구현하고 있다.

      • Migration() 추상 클래스의 생성자는 2개의 파라미터를 받는데,

        • 첫번째 파라미터는 변경 전 버전

        • 두번째 파라미터는 변경 후 버전

          을 받는다.

      • Migration() 추상 클래스는 migrate() 추상 메소드를 구현해야 하는데
        migrate() 메소드의 파라미터로 들어오는 SupportSQLiteDatabase 객체의
        execSQL() 메소드를 사용하여 변경사항을 제공할 수 있다.

        이떄 변경사항은 SQL코드의 형태로 execSQL() 메소드의 인자로 넣는다.

    • 이 부분에서 Migration을 DB에 적용시키고 있음

          return Room.databaseBuilder(
          context,
          AppDatabase::class.java,
          "BookSearchDB"
      )
          .addMigrations(migration_1_2)
          .build()
      • DB를 build()하기 전에 addMigrations() 메소드를 통해 구성한 Migration을
        DB에 적용시켜줄 수 있다.

        • 이렇게 할 경우,
          만약에 현재 앱에서 DB가 Version = 1이라면 Version = 2로 업그레이드 시켜주고,
          현재 앱에서 DB가 Version = 2 라면 그대로 생성된다.
    • 즉, 위의 내용을 확대하자면 DB의 버전이 1,2,3,4,5,6 이 있다고 했을 때,
      ( 1에 가까울수록 예전, 6에 가까울수록 최신이라고 가정 )
      현재 앱에서 DB의 버전이 3 버전일 경우,

      Version = 3 에서 Version = 4 로 업데이트,
      Version = 4 에서 Version = 5 로 업데이트,
      Version = 5 에서 Version = 6 로 업데이트

      의 과정이 순차적으로 일어나서 DB를 업그레이드 시키게 된다.


DB에서의 Thread

UI쓰래드에서만 동작하기엔 무거운 작업들의 경우 Thread를 나눠서 각자 처리하게 되는데,
DB에 데이터를 넣거나 데이터를 가져오는 등의 작업들도 여기에 포함된다.

따라서 DB를 다루는 기능이 필요하게 되면 Thread를 필수적으로 사용해야 된다.


Span --> Text의 일부분 혹은 전체에 특정한 효과를 주는 기능

참고 자료 : https://re-build.tistory.com/13

--> Text에 대해 세부적인 설정을 하도록 도와주는 기능이다.

예제 코드)

 ......

        val textView = findViewById<TextView>(R.id.textView)
                // textView의 내용은 "저는 친구가 적습니다."

        var ssb = SpannableStringBuilder(textView.text)
        ssb.setSpan(
        ForegroundColorSpan(getColor(R.color.red)),
        3,
        6,
        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

        textView.text = ssb
        
 ......       

예제 결과

Span 사용법

  1. SpannableStringBuilder() 메소드를 통해 변화를 주려는 text에 Span을 입힘

    --> 해당 메소드의 파라미터는 Span을 적용할 Text가 들어간다.

  2. setSpan을 통해 해당 text에서 Span을 통한 구체적인 변화를 파라미터로 넣음

    • 첫번째 파라미터로 어떤 효과의 Span을 적용할지를 선택

      --> ForegroundColorSpan(getColor(R.color.red)) --> 글자를 붉은색으로 변화시킬 것
      --> BackgroundColorSpan(getColor(R.color.red)) --> 배경색을 붉은색으로 변화시킬 것
      --> UnderlineSpan() --> 밑줄을 그어줄 것
      --> AbsoluteSizeSpan(100) --> 글자의 절대크기를 100으로 정해줄 것
      --> RelativeSizeSpan(1.5f) --> 기존의 글자크기에 1.5배의 크기로 할 것 ( 상대 크기 )
      --> StyleSpan(Typeface.ITALIC) --> 글자의 style을 ITALIC으로 해줄 것 --> BOLD, NORMAL, ITALIC등등
      이외에도 이미지, 클릭가능 텍스트, url 등으로도 설정이 가능하다.

    • 두번째 파라미터로 어디부터 적용할지 선택

    • 세번째 파라미터로 어디까지 적용할지 선택

      예를들어 10개의 문자에서 시작은 0이고, 끝이 8이면 0~7까지 8개의 문자가 적용됨

    • 네번쨰 파라미터로 Span의 포인트 또는 마크에 대한 제어를 위한 플래그를 지정

      --> 플래그 속성에는 INCLUSIVE와 EXCLUSIVE가 있음
      --> INCLUSIVE는 확장을 뜻하며, EXCLUSIVE는 단절을 뜻함

      예를 들어 )

      SPAN_INCLUSIVE_EXCLUSIVE 라는 플래그를 보시면, 앞부분에 확장성을 열어두고, 뒷부분에는 확장을 사용하지 않겠다는 뜻

      반대로 SPAN_EXCLUSIVE_INCLUSIVE 라는 플래그는 앞부분은 확장을 하지 않고, 뒷부분에 확장성을 열겠다는 뜻

      중요한 점은 플래그에 대한 설정은 고정적이지 않은, 변경될 수 있는 유동적인 값에만 적용된다는 점입니다.
      쉽게 확인 해보기 위해서는 EditText를 이용하여 앞이나 뒷부분에 글자를 추가해보시면서 두 속성의 차이를 비교해보면 좋습니다.


Ripple -> ( ShapeDrawable에서 클릭시 배경색 변경 기능 추가 )

ShapeDrawable에서 클릭시 배경색 변경과 같은 기능을 추가해준 것이다.

예제 코드


<?xml version="1.0" encoding="utf-8"?>

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/customButtomPressGray">
<!--    ripple내에 정의한 Shape Drawable이 클릭 상태일 때, 배경색이 ripple의 속성인 color의 색으로 변함-->
<!--    즉 ripple에 정의된 color는 눌렀을 때 나타나는 color임-->

    <item android:id="@android:id/background">
<!--        ripple을 쓸 때는 background를 따로 정의해야함 >>>>>> 이를 위한 item >>>>>> 이 안에 shape객체를 만들 것임 -->
<!--        이유는 >> ripple에서 정의한 background는 클릭시에 나타나는 색이므로, 이 그외의 background를 정의해야함-->

        <shape
            android:shape="rectangle">
            <solid android:color="@color/buttonGray"/>
            <corners android:radius="100dp"/>
            <stroke android:width="1dp"
                android:color="@color/customButtomPressGray"/>

<!--            solid > 내부색, corners > 모서리영역, stroke > 테두리선-->


        </shape>


    </item>

</ripple>
  • ripple의 속성에서 정의한 color가 바로 해당 ShapeDrawable을 클릭했을 때 변화할 배경색이다.

  • ripple을 사용할 때는 background를 따로 정의해줘야하는데, 이 역할을 위 코드의 item이 한다.
    ( 왜?? ,, ripple에서 정의한 color은 background를 클릭했을시에 나타나는 색이기 때문에 따로 background를 정의해줘야하는 것 )

  • 이후 item 컴포넌트 안에 shape를 정의하여 나머지는 ShapeDrawable과 같은 방식으로 하면된다.
    ( 이때 Shape안에 정의되는 Color는 기본 배경색이며, 이후 이 배경이 클릭될 때 ripple에서 정의한 Color로 바뀐다. )

Ripple을 추가한 ShapeDrawable사용

 <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/clearButton"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:background="@drawable/button_background"
                android:onClick="clearButtonClicked"
                android:stateListAnimator="@null"
                android:text="C"/>

위 코드 처럼 이렇게 추가해줄 수 있음
android:background="@drawable/button_background"

하지만 주의해야될 점이, 클릭했을 시에 배경색이 바꾸는 ripple을 해놨기 때문에 기존에 있던 클릭애니메이터를 없애줘야 적용이됨
android:stateListAnimator="@null"
--> 따라서 이렇게 애니메이터를 @null로 비워줬음


확장함수 만들기

아래의 예시들에서는 기본 타입에 대해서 사용했지만, 클래스를 포함해서 말그대로 대부분의 타입에 대해 확장함수를 만들 수 있다.

확장함수 예시1 --> 기본형

위의 경우 String에 대한 확장함수를 만들어준 것이다. > 즉, 이후 String을 반환하는 메소드 뒤에 확장함수로 사용 가능하다.
따라서 해당 확장함수는 String으로부터 사용가능하며, 해당 String의 값을 파라미터로 뒤에 람다함수를 실행( 블록부분 )한다.
그리고 따로 파라미터명을 지정해주지 않았기 때문에 this을 통해 해당 값에 접근이 가능하다.

확장함수 예시2 --> 리턴값 추가

위와 같이 리턴값을 준다.

확장함수 예시3 --> 어느 타입에 대한 확장함수?

위와 같이 함수 앞에 타입을 바꾸는 것으로 해당 타입의 확장함수로 만들 수 있다.
--> 위의 경우 Int.plusFive()이므로 해당 확장함수는 Int의 확장함수이다.

확장함수 예시4 --> 확장함수로 꼬리물기

확장함수는 일반적으로 위와같이 꼬리물기로 사용된다.

위의 함수를 해석해보면 3을 Int타입의 확장함수인 plusFive()의 파라미터로 줘서 람다함수를 실행시킨뒤, String타입의 "8"을 리턴하고
그렇게 리턴시킨 String 타입의 "8"을 String타입의 확장함수인 voice()의 파라미터로 줘서 람다함수를 실행시켜 위와 같은 결과가 나왔다.

확장함수 만들기

 fun 확장함수가붙을타입.확장함수명(): 리턴타입 { 람다함수 }
 // 확장함수가 붙을 타입 = 해당 확장함수를 사용할 수 있는 타입
 // 람다함수는 해당 확장함수를 사용한 값( 바로 앞의 함수의 리턴값이나 혹은 바로 앞의 변수의 값 등 )을 파라미터로 받아 실행된다. 그리고 따로 파라미터명을 지정하지 않았으므로 해당 값엔 this로 접근 가능하다.

TableLayout

관계형 DB와 같이 테이블 형태로 액티비티를 구성하는 레이아웃

  1. <TableLayout> 으로 테이블 레이아웃을 시작

  2. <TableLayout> 내부에 <TableRow> 를 통해 행을 만들어줌

  3. 이후 <TableRow> 내부에 들어가는 컴포넌트 들이 각각 그 행의 데이터로써 Horizontal로 orientation됨

예시 코드


<TableLayout
        android:id="@+id/keypadTableLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:shrinkColumns="*">
     
        

        <TableRow android:layout_weight="1">
            <!--            TableLayout에 들어가는 컴포넌트인 TableRow > 테이블의 행이라는 의미 -->
            <!--            이후 이 TableRow안에 들어가는 컴포넌트들이 column으로써 가로로 들어가게 됨-->


            <Button
                android:id="@+id/clearButton"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="C"
                android:textSize="24sp" />
                
           <Button
                android:id="@+id/clearButton"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="C"
                android:textSize="24sp" />
                
            <Button
                android:id="@+id/clearButton"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="C"
                android:textSize="24sp" />
          
            ...... 

	</TableRow>
    
  
    	<TableRow android:layout_weight="1">
        
        
         <Button
                android:id="@+id/clearButton"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="C"
                android:textSize="24sp" />
                
         <Button
                android:id="@+id/clearButton"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="C"
                android:textSize="24sp" />
    
   	 ......
    
   	 </TableRow>

</TableLayout>
  • 여기서 TableLayout
    android:shrinkColumns="*" 속성의 의미는
    해당 TableLayout 안에 Column에 대해 화면을 넘어가는 상황이 오면 각 컴포넌트들을 균일하게 축소하여 화면을 넘어가지 않게 만든다는 뜻
    ( 그리고 "*"을 준 것은 레이아웃 내에 특정 컴포넌트가 아닌 TableLayout내의 전체 컴포넌트에 대해 적용시킨다는 뜻이다. )

  • TableRow
    android:layout_weight="1" 속성은 가중치를 나타내며
    해당 가중치를 바탕으로 TableRow들이 공간을 나눠갖고 있음


Vector asset에서 내장 이미지 생성해서 가져오기

  1. drawable폴더에서 우클릭, New에서 Vector asset클릭
  1. 열린 asset Studio에서 원하는 이미지 파일을 구성해서 생성
  1. 아래와 같이 생성한 이미지 xml파일이 drawable폴더에 생성됨

코드 설명

MainActivity.mk

package fastcampus.aop.part1.aop_part2_chapter4

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.room.Room
import fastcampus.aop.part1.aop_part2_chapter4.model.History
import org.w3c.dom.Text
import java.lang.NumberFormatException

class MainActivity : AppCompatActivity() {

    private val expressionTextView by lazy {
        findViewById<TextView>(R.id.expressionTextView)
    }

    private val resultTextView by lazy {
        findViewById<TextView>(R.id.resultTextView)
    }

    private val historyLayout: View by lazy {
       findViewById( R.id.historyLayout)
    }
    private val historyLinearLayout: LinearLayout by lazy {
        findViewById( R.id.historyLinearLayout)
    }

    lateinit var db: AppDatabase
    // Room을 이용하기 위해 AppDatabase를 담을 변수 생성

    private var isOperator = false
    private var hasOperator = false


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

        db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "historyDB"
            ).build()
        // databaseBuilder를 이용해여 데이터베이스 생성 --> 첫번째 파라미터로 해당 앱의 위치,
        // 두번째 파라미터로 room의 @Database를 선언한 추상클래스,
        // 세번째 파라미터로 DB의 이름을 넣어준다.
        // 이후 마지막에 build()메소드를 통해 해당 데이터베이스를 생성해준다.
        
    }


    fun buttonClicked(v: View) {
        when (v.id) {
            R.id.button0 -> numberButtonClicked("0")
            R.id.button1 -> numberButtonClicked("1")
            R.id.button2 -> numberButtonClicked("2")
            R.id.button3 -> numberButtonClicked("3")
            R.id.button4 -> numberButtonClicked("4")
            R.id.button5 -> numberButtonClicked("5")
            R.id.button6 -> numberButtonClicked("6")
            R.id.button7 -> numberButtonClicked("7")
            R.id.button8 -> numberButtonClicked("8")
            R.id.button9 -> numberButtonClicked("9")
            R.id.buttonPlus -> operatorButtonClicked("+")
            R.id.buttonMinus -> operatorButtonClicked("-")
            R.id.buttonMulti -> operatorButtonClicked("*")
            R.id.buttonDivider -> operatorButtonClicked("/")
            R.id.buttonModulo -> operatorButtonClicked("%")

        }

    }

    private fun numberButtonClicked(number: String) {
        // 숫자는 최대 15자리까지만 허용할 것임

        if (isOperator) {
            expressionTextView.append(" ")

        }

        isOperator = false


        val expressionText = expressionTextView.text.split(" ")
        // 띄어쓰기를 기준으로 숫자와 연산자를 구분할 것임
        // expressionText는 expressionTextView의 text속성이 " "을 기준으로 나눈 리스트형임

        if (expressionText.isNotEmpty() && expressionText.last().length >= 15) {
            // list.last() > 해당 리스트의 마지막 항목을 리턴함
            Toast.makeText(this, "15자리 까지만 사용할 수 있습니다.", Toast.LENGTH_LONG).show()
            return
            // isNoteEmpty를 한 이유는 숫자 버튼을 처음 누를 때 Text는 비어있으므로 이대로 last()를 하면 아무 값도 리턴되지 않는다.
            // 이것을 방지 한 것 ( 즉, 처음 숫자를 누를 때에는 isNotEmpty에서 해당 조건문이 걸러짐 )

        } else if (expressionText.last().isEmpty() && number == "0") {
            Toast.makeText(this, "0은 제일 앞에 올 수 없습니다.", Toast.LENGTH_SHORT).show()
        }

        expressionTextView.append(number)
        // append > TextView의 Text속성에 파라미터의 값을 뒤에 붙여줌

        resultTextView.text = calculateExpression()
        // TODO resultTextView 실시간으로 계산 결과를 넣어야하는 기능

    }

    private fun operatorButtonClicked(operator: String) {
        // 연산자는 두번 연속 누를 수 없음
        if (expressionTextView.text.isEmpty()) {
            return
        }

        // when의 활용 > when 내부에서 
        // "조건문 -> 조건문이true였을경우의실행문" 으로 이루어져있는데
        // 아래와 같이 when문에 따로 파라미터를 지정하지 않아도, 조건문부분이 true이면 실행문 부분이 실행된다
        when {
            isOperator -> {
                val text = expressionTextView.text.toString()
                expressionTextView.text = text.dropLast(1) + operator
                // 오퍼레이터를 연속으로 입력할 경우, 오류가 발생하는 것이 아니라 해당 오퍼레이터를 교체해주도록 만들기 위해 이렇게 코딩함
                // 기존에 있던 (맨 뒤에 있는) 오퍼레이터를 지우고 그 뒤에 새롭게 입력된 오퍼레이터를 넣어서 TextView의 text속성에 갱신
            }
            hasOperator -> {
                Toast.makeText(this, "연산자는 한 번만 사용할 수 있습니다.", Toast.LENGTH_SHORT).show()
                return

            }
            else -> {
                expressionTextView.append(" $operator")
                // 숫자 이후 처음 연산자를 입력할 때 해당 코드 실행
            }
        }


        // Span 이용 >> 연산자는 초록색으로 따로 칠해서 구현할 것 >> 하나의 TextView내에서 특정 문자들만 색을 칠해줄 수 있음
        val ssb = SpannableStringBuilder(expressionTextView.text)
        ssb.setSpan(
            ForegroundColorSpan(getColor(R.color.green)),
            expressionTextView.text.length - 1,
            expressionTextView.text.length,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        )

        // Span >> 글자에 대해 세부적인 설정을 할 수 있게 해줌 ( 특정문자에 대해 )

        // SpannableStringBuilder를 통해 Span설정 생성을 위한 객체를 받음, 해당 Builer는 파라미터로 해당 Span을 적용시킬 Text를 받음
        // 이후 setSpan을 통해 해당 Span을 설정해줌
        // setSpan의 첫번째 파라미터는 ForegroundColorSpan을 통해 글자색을 설정 혹은 BackgroundColorSpan을 통해 배경색을 설정, 두번째 파라미터는 해당 색으로 칠해질 시작점, 세번째 파라미터는 해당 색으로 칠해질 끝점( 시작은 0일 때 10개에서 8까지면 0~7까지 8개가 칠해짐 ), 네번쨰 파라미터는 Span의 설정을 넣음

        expressionTextView.text = ssb

        isOperator = true
        hasOperator = true
    }


    fun resultButtonClicked(v: View) {
        val expressionTexts = expressionTextView.text.split(" ")

        if (expressionTextView.text.isEmpty() || expressionTexts.size == 1) {
            return
        }

        if (expressionTexts.size != 3 && hasOperator) {
            Toast.makeText(this, "아직 완성되지 않은 수식입니다.", Toast.LENGTH_SHORT).show()
            return
        }

        if (!expressionTexts[0].isNumber() || !expressionTexts[2].isNumber()) {
            // isNumber()는 내가 만든 확장함수임
            return
        }

        val expressionText = expressionTextView.text.toString()
        // DB에 저장하기 위해 미리 가져옴
        val resultText = calculateExpression()


        //TODO 디비에 넣어주는 부분
        Thread(Runnable {
            db.historyDao().insertHistory(History(null, expressionText, resultText))
        }).start()
        // DB에서 넣고 빼는 등의 작업들은 UI쓰래드에서 처리하면 안되고, 위와 같이 따로 쓰래드를 만들어서 처리해줘야 함 -> 앱의 처리효율 및 앱이 멈추지 않도록


        resultTextView.text = ""
        expressionTextView.text = resultText

        isOperator = false
        hasOperator = false

    }

    private fun calculateExpression(): String {
        val expressionTexts = expressionTextView.text.split(" ")

        if (hasOperator.not() || expressionTexts.size != 3) {
            return ""
        } else if (!expressionTexts[0].isNumber() || !expressionTexts[2].isNumber()) {
            // isNumber()는 내가 만든 확장함수임
            return ""
        }

        val exp1 = expressionTexts[0].toBigInteger()
        val exp2 = expressionTexts[2].toBigInteger()
        val op = expressionTexts[1]
        return when (op) {
            "+" -> (exp1 + exp2).toString()
            "-" -> (exp1 - exp2).toString()
            "*" -> (exp1 * exp2).toString()
            "/" -> (exp1 / exp2).toString()
            "%" -> (exp1 % exp2).toString()
            else -> ""
            // 람다함수의 감을 익히자 ! > 람다함수 부분에 나타난 유일한 데이터는 그냥 람다함수의 리턴처럼 여겨진다.

        }
    }

    fun clearButtonClicked(v: View) {
        expressionTextView.text = ""
        resultTextView.text = ""
        isOperator = false
        hasOperator = false

    }




    // 아래와 같이 파라미터로 View를 받는 리스너 메소드를 만들면 xml파일의 컴포넌트에 대해
    fun historyButtonClicked(v: View) {
        historyLayout.isVisible = true

        historyLinearLayout.removeAllViews()
        //해당 LinearLayout 내부의 모든 View들 삭제

        // TODO 디비에서 모든 기록 가져오기
        // TODO 뷰에 모든 기록 할당하기
        Thread(Runnable {
            db.historyDao().getAll().reversed().forEach{
                
                // runOnUiThread를 통해 UI쓰래드를 열어줌 -> 새로운 쓰래드로는 앱의 내용에 영향을 줄 수 없음 -> 새로운 쓰래드에서 UI쓰래드로 데이터를 넘겨줘야함
                // runOnUiThread를 구현하는 코드 안에는 Handler가 있음 -> Handler는 쓰래드끼리 데이터를 전달할 수  있게 해줌 -> 이를 통해 새로운 쓰래드에서 UI쓰래드로 데이터를 전달하여 UI쓰래드에서 처리하도록 하는 것
                runOnUiThread{
                    

                    val historyView = LayoutInflater.from(this).inflate(R.layout.history_raw,null,false)
                    historyView.findViewById<TextView>(R.id.expressionTextView).text = it.expression
                    historyView.findViewById<TextView>(R.id.resultTextView).text = "= ${it.result}"

                    historyLinearLayout.addView(historyView)

                    // LayoutInflator를 통해 Layout을 가져오는 과정 LayoutInflater.from()은 매개변수로 현재 위치를 받고 이후 inflate메소드를 이용하여 특정 xml파일을 불러올 수 있다.( 첫번쨰 파라미터로 불러올 xml파일 두번쨰와 세번쨰는 나중에 설명할 것임 )
                    // 이후 아래와 같은 방식으로 불러온 xml파일 내부의 View들에 대해 id로 접근할 수 있으며
                    // 이렇게 구성된 Layout은 View로써 main이 되는 xml파일에 addView()메소드의 파라미터로 넣는 것으로 넣어줄 수 있다.
                }

            }
            // 가져올 당시에 0?1?번부터 가져오는데, 이 경우 최신 것이 아래로 가게됨
            // 우리는 최신 것을 위로 만들어주기 위해서 가져온 리스트를
        }).start()
    }


    fun closeHistoryButtonClicked(v: View) {
        historyLayout.isVisible = false

    }


    fun historyClearButtonClicked(v: View) {

        //TODO 디비에서 모든 기록 삭제
        //TODO 뷰에서서 모든 기록 삭제


        historyLinearLayout.removeAllViews()

        Thread(Runnable {
            db.historyDao().deleteAll()
        }).start()
    }

}



// 확장함수를 만드는 방법
// 아래의 경우 String에 대한 확장함수를 만들어준 것이다. > 즉, 이후 String을 반환하는 메소드 뒤에 확장함수로 사용 가능하다.
// 따라서 해당 확장함수는 String으로부터 사용가능하며, 해당 String의 값을 파라미터로 뒤에 람다함수를 실행( 블록부분 ) Boolean을 리턴한다.
// 그리고 따로 파라미터명을 지정해주지 않았기 때문에 this을 통해 해당 값에 접근이 가능하다.
fun String.isNumber(): Boolean{
    return try{
        this.toBigInteger()
        // 넘겨받은 String을 int형으로 변환
        // toInt()가 1억까지 정도라면 toBigInteger()는 무한까지 가능하다.
        
        true
        // 해당 블록에서 문제가 없었다면 true 반환
        
    }catch (e: NumberFormatException){
        // 어떤 오류가 나타날수 있는지에 대한 부분은 해당 함수들의 본 코드를 보면 나와있다.
        false

    }
    // 람다함수의 감을 익히자 ! > 람다함수 부분에 나타난 유일한 데이터는 그냥 람다함수의 리턴처럼 여겨진다.
}


주목해야 될 부분

같은 기능의 여러 버튼이 있을 경우 findViewById를 하는 팁과 ClickLisener를 붙여주는 팁


......

    fun buttonClicked(v: View) {
        when (v.id) {
            R.id.button0 -> numberButtonClicked("0")
            R.id.button1 -> numberButtonClicked("1")
            R.id.button2 -> numberButtonClicked("2")
            R.id.button3 -> numberButtonClicked("3")
            R.id.button4 -> numberButtonClicked("4")
            R.id.button5 -> numberButtonClicked("5")
            R.id.button6 -> numberButtonClicked("6")
            R.id.button7 -> numberButtonClicked("7")
            R.id.button8 -> numberButtonClicked("8")
            R.id.button9 -> numberButtonClicked("9")
            R.id.buttonPlus -> operatorButtonClicked("+")
            R.id.buttonMinus -> operatorButtonClicked("-")
            R.id.buttonMulti -> operatorButtonClicked("*")
            R.id.buttonDivider -> operatorButtonClicked("/")
            R.id.buttonModulo -> operatorButtonClicked("%")
            
......
            
            
private fun numberButtonClicked(number: String) {
        // 숫자는 최대 15자리까지만 허용할 것임

......

 private fun operatorButtonClicked(operator: String) {
        // 연산자는 두번 연속 누를 수 없음
        
        
......        

        }

    }

--> 이렇게 해놓은 뒤 xml차원에서 onClick 속성에 해당 buttonClicked를 설정해주면, 해당 버튼을 매칭함과 동시에 ClickLisener설정까지 할 수 있다.

Room기능을 이용한 DB 생성 시점


......

 lateinit var db: AppDatabase
    // Room을 이용하기 위해 AppDatabase를 담을 변수 생성

......


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

        db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "historyDB"
            ).build()
        // databaseBuilder를 이용해여 데이터베이스 생성 --> 첫번째 파라미터로 해당 앱의 위치 , 
        // 두번째 파라미터로 room의 @Database를 선언한 추상클래스, 
        // 세번째 파라미터로 DB의 이름을 넣어준다.
        // 이후 마지막에 build()메소드를 통해 해당 데이터베이스를 생성해준다.
        
    }
    
    
......    

TextView.append( String타입데이터 ) --> TextView의 text 뒤에 붙여줌

        expressionTextView.append(number)
        // append > TextView의 Text속성에 파라미터의 값을 뒤에 붙여줌

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <View
        android:id="@+id/topLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/keypadTableLayout"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_weight="1" />
    <!--    constarintVertical_weight를 줘서 수직에 대한 기중치를 설정함-->
    <!--    가중치를 바탕으로 크기를 조절할 것이기 때문에 height를 0dp 로 설정-->
    <!--    결과적으로 두 레이아수이 각각 1과 1.5의 가중치를 받아 2:3의 비율로 화면을 나눠가지게 됨-->

    <TextView
        android:id="@+id/expressionTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:layout_marginTop="44dp"
        android:layout_marginEnd="15dp"
        android:gravity="end"
        android:textColor="@color/black"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/resultTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:layout_marginEnd="15dp"
        android:layout_marginBottom="15dp"
        android:gravity="end"
        android:textColor="#aaaaaa"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@id/keypadTableLayout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <TableLayout
        android:id="@+id/keypadTableLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:paddingStart="15dp"
        android:paddingTop="21dp"
        android:paddingEnd="15dp"
        android:paddingBottom="21dp"
        android:shrinkColumns="*"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/topLayout"
        app:layout_constraintVertical_weight="1.5">
        <!--    가중치를 바탕으로 크기를 조절할 것이기 때문에 height를 0dp 로 설정-->
        <!--    constarintVertical_weight를 줘서 수직에 대한 기중치를 설정함-->

        <!--        android:shrinkColumns="*" 속성의 의미는 해당 테이블레이아웃 안에 Column에 대해-->
        <!--        화면을 넘어가는 상황이 오면 각 컴포넌트들을 균일하게 축소하여 화면을 넘어가지 않게 만든다는 뜻-->
        <!--        그리고 "*"을 준 것은 레이아웃내의 특정 컴포넌트가 아닌 레이아웃내의 전체 컴포넌트에 대해 적용시킨다는 뜻이다.-->

        <TableRow android:layout_weight="1">
            <!--            TableLayout에 들어가는 컴포넌트인 TableRow > 테이블의 행이라는 의미 -->
            <!--            이후 이 TableRow안에 들어가는 컴포넌트들이 column으로써 가로로 들어가게 됨-->
            <!--          가중치를 바탕으로 TableRow들이 공간을 나눠갖고 있음 -->

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/clearButton"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="clearButtonClicked"
                android:stateListAnimator="@null"
                android:text="C"
                android:textSize="24sp" />
            <!--            기본 테마(theme)인 MaterialComponent테마에서 벗어나 색을 설정하기 위해 AppCompatButton 사용-->
            <!--            onClick속성 > 이번 예제와 같이 리스너가 필요한 컴포넌트가 너무 많을 것으로 예상될 경우, 소스코드에서 onClickLisener를 직접 불러오는 방식으로 사용됨-->

            <!--            android:stateListAnimator="@null" > 버튼에는 기본적으로 장착되어있는 애니메이터가 있는데 버튼을 눌렀을 때 누른듯한 표현이 나오는게 그것이다.-->
            <!--            그 표현을 없애겠다는 뜻 >> 우리는 밋밋하지만, 눌렀을 때 배경색을 바꾸는 버튼을 만들 것이기 떄문-->

            <androidx.appcompat.widget.AppCompatButton
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:clickable="false"
                android:enabled="false"
                android:stateListAnimator="@null"
                android:text="()"
                android:textColor="@color/green"
                android:textSize="24sp" />
            <!--                android:onClick="buttonClicked" -->
            <!--            이 버튼은 이번에 사용하지 않을 것이므로 리스너를 빼주고, enabled를 false로 하여 사용못하게 그리고 clickable을 false로 줘서 클릭 못하게 해준다.-->


            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/buttonModulo"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="%"
                android:textColor="@color/green"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/buttonDivider"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="/"
                android:textColor="@color/green"
                android:textSize="24sp" />


        </TableRow>

        <TableRow android:layout_weight="1">

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/button7"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="7"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/button8"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="8"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/button9"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="9"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/buttonMulti"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="X"
                android:textColor="@color/green"
                android:textSize="24sp" />


        </TableRow>

        <TableRow android:layout_weight="1">

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/button4"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="4"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/button5"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="5"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/button6"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="6"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/buttonMinus"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="-"
                android:textColor="@color/green"
                android:textSize="24sp" />

        </TableRow>

        <TableRow android:layout_weight="1">

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/button1"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="1"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/button2"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="2"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/button3"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="3"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/buttonPlus"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="+"
                android:textColor="@color/green"
                android:textSize="24sp" />

        </TableRow>

        <TableRow android:layout_weight="1">

            <ImageButton
                android:id="@+id/historyButton"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="historyButtonClicked"
                android:src="@drawable/ic_baseline_access_time_24"
                android:stateListAnimator="@null"
                android:textSize="24sp" />
            <!--            해당 버튼에는 이미지파일을 넣을 것이므로 ImageButton으로 설정해줌-->

            <!--            안드로이드 스튜디오에 기본적으로 내장되어 있는 이미지파일을 가져와서 쓸 것임-->
            <!--            drawable폴더에 우클릭 > New탭 안에 > Vector Asset클릭 -> 안드로이드 안에 원래 내장되어있던 다양한 아이콘들을 사용할 수 있음-->

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/button0"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:onClick="buttonClicked"
                android:stateListAnimator="@null"
                android:text="0"
                android:textSize="24sp" />

            <androidx.appcompat.widget.AppCompatButton
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background"
                android:clickable="false"
                android:enabled="false"
                android:stateListAnimator="@null"
                android:text="."
                android:textSize="24sp" />
            <!--            이 버튼도 이번에 사용하지 않읋 것-->

            <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/resultButton"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_margin="7dp"
                android:background="@drawable/button_background_green"
                android:onClick="resultButtonClicked"
                android:stateListAnimator="@null"
                android:text="="
                android:textColor="@color/white"
                android:textSize="24sp" />

        </TableRow>

    </TableLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/historyLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/white"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@+id/keypadTableLayout"
        tools:visibility="visible">

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/closeButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:background="@null"
            android:onClick="closeHistoryButtonClicked"
            android:stateListAnimator="@null"
            android:text="닫기"
            android:textColor="@color/black"
            android:textSize="28sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


        <ScrollView
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_margin="15dp"
            app:layout_constraintBottom_toTopOf="@+id/historyClearButton"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/closeButton">

            <LinearLayout
                android:id="@+id/historyLinearLayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">


            </LinearLayout>

        </ScrollView>

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/historyClearButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="47dp"
            android:layout_marginEnd="47dp"
            android:layout_marginBottom="38dp"
            android:background="@drawable/button_background_green"
            android:onClick="historyClearButtonClicked"
            android:stateListAnimator="@null"
            android:text="계산기록 삭제"
            android:textColor="@color/white"
            android:textSize="18sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />


    </androidx.constraintlayout.widget.ConstraintLayout>


</androidx.constraintlayout.widget.ConstraintLayout>

Appdatabase.kt 파일 (Room의 기능으로 데이터베이스를 생성하는 파일)


package fastcampus.aop.part1.aop_part2_chapter4

import androidx.room.Database
import androidx.room.RoomDatabase
import fastcampus.aop.part1.aop_part2_chapter4.dao.HistoryDao
import fastcampus.aop.part1.aop_part2_chapter4.model.History


@Database(entities = [History::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    
    abstract fun historyDao(): HistoryDao

}

HistoryDao.kt파일 (Room의 기능으로 데이터베이스를 제어하는 메소드가 들어있는 파일)


package fastcampus.aop.part1.aop_part2_chapter4.dao

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import fastcampus.aop.part1.aop_part2_chapter4.model.History

// Dao란 Room에서 데이터를 주고받을 수 있게 해주는 기능을 말한다.

// gradle에서 Room을 가져왔으므로 @Dao로 사용할 수 있다.
@Dao
interface HistoryDao {

    @Query("SELECT * FROM history")
    fun getAll(): List<History>
    // 해당 함수를 사용하면 위에 설정한 Query를 통해 데이터를 가져오게 됨
    //@Dao선언을 했기 때문에 Room의 데이터 인터페이스가 된것이고, 그래서 위와 같이 @Query를 사용할 수 있는 것임

    @Insert
    fun insertHistory(history: History)
    // History데이터 클래스로 데이터를 받아서 넣는다. ( @Insert )

    @Query("DELETE FROM history")
    fun deleteAll()
    // @Query를 이용 ->  모든 데이터 삭제

    @Delete
    fun delete(history: History)
    //History데이터 클래스로 데이터를 받아 특정 데이터만을 삭제

    @Query("SELECT * FROM history WHERE result LIKE :result")
    fun findByResult(result: String): List<History>
    // result값을 파라미터로 받아 Query에 넣어 해당 데이터만을 가져오게 만듬
    // ":파라미터명" 을 통해 Query에서 파라미터에 접근할 수 있음
    
    @Query("SELECT * FROM history WHERE result LIKE :result LIMIT 1")
    fun findByResultOnlyOne(result: String): History
    // 위와 같지만 하나의 데이터만 반환됨

}

History.kt (Room의 기능으로 데이터를 담기 위한 data class 파일)


package fastcampus.aop.part1.aop_part2_chapter4.model

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

// 해당 패키지는 내가 새로 만든 pachage
// 해당 파일은 DB에 넣기 위한 MODEL을 만들기 위한 파일임 ( ModelClass >> 데이터를 담기위한 Class )

// 따라서 data class 를 만들 것임


@Entity

data class History(
    @PrimaryKey val uid: Int?,
    // PrimaryKey로 설정했으므로 null로 넣어도 들어갈때 자동으로 하나씩 올라감

    @ColumnInfo(name="expression") val expression: String?,
    @ColumnInfo(name = "result") val result: String?


)

// 각 변수가 var이 아닌 val로 만들어 졌기 때문에 setter는 따로 생성되지 않음
// data class로 만들어졌기 떄문에 toString(), equals(), hashcode, copy가 가동으로 생성됨 + getter도 ,, val이르모 setter는 생성안됨 ( 수정이 불가하기 떄문 )

HistoryRaw.xml ( LayoutInflater로 삽입하기 위해 만든 xml 파일 )


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/expressionTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:layout_marginTop="44dp"
        android:layout_marginEnd="15dp"
        android:gravity="end"
        android:textColor="@color/black"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="9999" />

    <TextView
        android:id="@+id/resultTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:layout_marginEnd="15dp"
        android:layout_marginBottom="15dp"
        android:gravity="end"
        android:textColor="#aaaaaa"
        android:textSize="20sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/expressionTextView"
        tools:text="9999"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

button_background.xml ( ripple을 사용함, activity_main.xml 파일의 컴포넌트에 background로 사용하기 위해서 만든 파일 )

<?xml version="1.0" encoding="utf-8"?>

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/customButtomPressGray">
<!--    해당 color은 내가 정의한 것-->
<!--    ripple내에 정의한 Shape Drawable이 클릭 상태일 때, 배경색이 ripple의 속성인 color의 색으로 변함-->
<!--    즉 ripple에 정의된 color는 눌렀을 때 나타나는 color임-->

    <item android:id="@android:id/background">
<!--        ripple을 쓸 때는 background를 따로 정의해야함 -> 이를 위한 item > 이 안에 shape객체를 만들 것-->
<!--        이유는 >> ripple에서 정의한 background는 클릭시에 나타나는 색이므로, 이 그외의 background를 정의해야함-->

        <shape
            android:shape="rectangle">
            <solid android:color="@color/buttonGray"/>
            <corners android:radius="100dp"/>
            <stroke android:width="1dp"
                android:color="@color/customButtomPressGray"/>

<!--            solid > 내부색, corners > 모서리영역, stroke > 테두리선-->


        </shape>


    </item>

</ripple>
profile
ㅎㅎ

0개의 댓글