0. 소개

 

MotionLayout은 ConstraintLayout 안에 있는 객체들에 대해서 XML 파일들만 추가해서 간단하게 레이아웃 애니메이션 효과를 줄 수 있는 툴 정도로 이해하면 될 것 같다. 예제로 구글 소개글에 있는 예제들 봐도 되고 아래 gif 이미지를 참고해도 좋다. 2018년 말에 나온 기능인데 이제 와서 글을 쓰고 있으니 아주 뒷북인 감이 없지 않다.

 

스와이프로 빨간 사각형을 움직이는 애니메이션을 줄 수 있다.

1. 원리

 

MotionLayout 은 완전히 새로운 기능으로 도입 된 것은 아니고 원래 ConstraintLayout에서 애니메이션 효과를 주기 위해 사용한 ConstraintSet + TransitionManager를 좀 더 쉽게 사용할 수 있는 툴로 도입 됐다. ConstarintSet + TransitionManager에 대해 생소하신 분들은 이 유튜브 영상을 참고하자. 기존 효과보다 더 좋아진 점은 ConstraintSet + TransitionManager 조합에서는 간단한 효과의 경우에도 액티비티, 프래그먼트단에서 코드를 추가해야 했는데 MotionLayout을 사용하면 XML 코드단에서만 수정하면 돼서 변경의 범위를 최소화 할 수 있는 것 같다.

 

ConstarintLayout의 확장 기능으로 도입된 만큼 MotionLayout은 ConstraintLayout의 일부 요소들을 상속 받고 있다. 아래 그림에서 MotionLayout 의 기본 요소인 MotionScene이 ConstraintSet의 속성들을 포함하고 있는 것을 볼 수 있다. 실제 코드에서는 이 요소를 활용해 ConstraintLayout 내부의 객체들의 효과를 주는 일을 한다. 그 아래 Transition 속성을 보면 OnClick과 OnSwipe가 있는데 이는 클릭과 스와이프 인터랙션에 대해서 콜백을 줄 수 있는 것으로 이해하면 된다. 이 글에서는 MotionLayout은 ConstraintLayout 의 속성들을 이용해 애니메이션 할 수 있다는 점이란 것을 기억하고 자세한 내용은 개발 문서를 참고하도록 하자. 

 

2. 예제

 

MotionLayout을 사용하려면 우선 라이브러리를 추가해야한다. build.gradle에 아래의 코드를 추가해 최신 ConstraintLayout 소스를 불러오자. 번외로 2018년도 즈음에 MotionLayout이 ConstraintLayout 2.0.0 라이브러리에 포함되기 시작했는데 아직도 beta 버전에 머무르고 있는거 보니 다른 feature들과 같이 정식으로 릴리즈 되려는 모양인가보다.

 

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'

 

정상적으로 라이브러리를 불러 왔다면 이제 레이아웃 파일을 수정할 때다. 아래 코드는 기존에 있던 xml 파일을 MotionLayout을 적용할 수 있도록 변경한 것이다.

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout 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"
    app:layoutDescription="@anim/motion_scene"
    tools:context=".MainActivity">
    <View
        android:id="@+id/button"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:background="@color/colorAccent"
        android:text="Button"/>
</androidx.constraintlayout.motion.widget.MotionLayout>

 

원래는 ConstraintLayout을 사용했었는데 MotionLayout을 사용하고자 클래스 이름을 MotionLayout으로 변경했다. MotionLayout이 ConstraintLayout을 상속받은 클래스이기 때문에 자식 뷰에서 특별히 바꿔야할 것은 없다. 새롭게 추가한 코드는 app:layoutDescription인데 여기에 Motion 효과를 명시한 xml 파일을 넣었다. 

 

motion_scene 파일은 다음과 같다.

 

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">
    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="1000">
        <OnSwipe
            motion:touchAnchorId="@+id/button"
            motion:touchAnchorSide="right"
            motion:dragDirection="dragRight" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginEnd="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>
</MotionScene>

Transition 속성을 보면 motion:constraintSetStart와 motion:constraintSetEnd 속성이 있는데 이는 애니메이션 효과 동안 constraint의 시작과 끝에 대한 정보를 나타낸다. 아래 코드 보면 두개의 ConstraintSet이 있는 것을 볼 수 있는데 시작할 때의 값과 끝의 값이 다른 것을 알 수 있다. 이 포스트 상단에 있는 gif 파일과 일치하는 것을 볼 수 있다.

 

그 아래 OnSwipe는 특정 뷰를 스와이프 할 때 줄 수 있는 효과를 명시했다. touchAnchorId의 값이 +@id/button으로 설정돼있는데 이는 MotionLayout에서 button 이란 id를 가진 뷰에게는 swipe 효과를 줄 것이라는 뜻이다. motion:touchAnchorSide와 motion:dragDirection이 있는데 이 값들을 이용해서 스와이프에 추가로 효과를 줄 수 있다.

 

3. 짧은 평

 

Android Studio 4.0에서는 XML파일에 MotionLayout 미리보기 화면에서도 애니메이션 효과를 보여줄 예정이라고 하니(4.0-beta 버전 참고) 구글에서도 MotionLayout을 사용하는 것을 적극 권장하는 것 같다. 앞으로 MotionLayout에서 추가된 기능이 나올 것 같으니 지금부터 프로젝트에 도입하는 것을 목표로 해야겠다.

px, dp, sp 개념 정리

컴퓨터공부/안드로이드 2019. 2. 16. 16:04 Posted by 아는 개발자

XML 파일 안에서 UI 컴포넌트의 크기를 정할 때 dp, sp, px 단위의 원리를 제대로 알지 않고 사용하는 경우가 있는데 이렇게 막 사용하다 보면 디바이스 해상도가 변경 될 때 화면이 깨지는 경우가 생긴다. 이번 포스트에서는 지금까지 막무가내로 사용해온(?) dp, sp, px 단위를 총 정리 해보려고 한다.


0. px (Pixel)



주소화 할 수 있는 화면의 가장 작은 단위. 점을 찍을 수 있는 가장 작은 단위라고 생각하면 된다. 화면 해상도와 관계 없이 컴포넌트의 크기를 절대값으로 세팅할 수 있으며 위의 그림으로 알 수 있듯이 오른쪽으로 갈 수록 폰의 해상도가 높아지면서 텍스트 상자와 이미지 아이콘이 작아진 것을 알 수 있는데 값을 픽셀 단위로 설정했기 때문에 그렇다. 이런 단위로는 여러 디바이스 환경마다 아이콘의 크기가 천차만별이 될 수 있어서 안드로이드 환경에서는 잘 사용하지 않는다.


1. dp (Density-independent Pixels)


화면의 해상도값에 따라서 크기가 조정되는 단위다. 160dpi 스크린 해상도에서는 1px만큼의 크기를 가지고 해상도 값이 커질수록 픽셀의 값이 커진다. 320dpi 해상도면 2px 만큼의 크기를 가지고 480dpi 면 3px만큼 가지는 원리다. 위의 그림에서도 보이듯이 해상도가 높아져도 텍스트상자와 이미지의 크기가 일정한 것을 알 수 있는데 이는 해상도에 따라서 크기가 재설정 됐기 때문이다. 해상도에 따라 크기가 자동으로 늘어나고 줄어들 수도 있으니 스펙이 다양한 안드로이드 디바이스 환경에서 적용할 때 유용한 단위다.


2.  sp (Scale-independent Pixels)


dp랑 비슷한 원리로 동작하며 사용자의 글짜 크기(font-size)를 조정할 때 사용되는 단위다. 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

AsyncTask  (0) 2019.03.13
ViewPager와 PageAdapter  (0) 2019.03.05
px, dp, sp 개념 정리  (0) 2019.02.16
Device screen dpi 값에 따라 처리하기  (0) 2019.02.16
안드로이드 선 추가하기  (0) 2019.01.26
SharedPreference  (0) 2019.01.10

안드로이드 선 추가하기

컴퓨터공부/안드로이드 2019. 1. 26. 15:19 Posted by 아는 개발자

안드로이드 앱을 사용하다보면 기사나 댓글 화면에는 여러 개의 아이템을 구분짓기 위해서 빨간 상자로 표시한 것처럼 선을 사용한다.

 

저렇게 단순한 선 정도는 따로 그림 파일을 가져올 필요 없고 안드로이드에서 만들어 줄 수 있다. XML 파일로 만들면 된다. 선을 표현하는 방법은 다양하게 있는데 가장 쉬운 방법은 View 클래스에 백그라운드 컬러색을 넣고 가로와 세로 영역을 잡아주는 방법이 있다.

 

하지만 나는 귀찮더라도 resource에 파일을 추가해서 간단히 세로가 짧은 사각형을 만드는 것으로 구현 해봤다. 

 

1. 코드

 

line_sample.xml

<shape
    android:shape="rectangle"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#000000" />
</shape>

 

위 리소스 파일은 그냥 검은색 사각형이다. 이 검은색 사각형을 실제 화면 뷰를 담당하는 XML 파일에 가로영역을 길게하고 세로 영역을 짧게 하면 선처럼 보이게 된다. 아래 코드처럼 ImageView에 넣어도 되고 아니면 View 클래스에 넣어도 괜찮다.

 

<ImageView
        android:src="@drawable/line_sample"
        android:layout_width="match_parent"
        android:layout_height="5dp"
        android:layout_margin="20dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

 

적용하면 아래처럼 정중앙을 관통하는 선이 하나 생긴다.

 

 

2. 확장 

 

리소스 파일의 property 값을 수정해 선의 가장자리를 더 둥글게 만들 수 있다. 실제로 꼭지점에 radius값을 넣어준 사각형을 만드는 것과 동일한 원리다. 코드에 한줄만 추가하면 된다.

 

<shape
    android:shape="rectangle"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="10dp"/>
    <solid android:color="#000000" />
</shape>

 

 그 결과 아래 그림처럼 양끝이 둥근 선이 만들어졌다.

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

AsyncTask  (0) 2019.03.13
ViewPager와 PageAdapter  (0) 2019.03.05
px, dp, sp 개념 정리  (0) 2019.02.16
Device screen dpi 값에 따라 처리하기  (0) 2019.02.16
안드로이드 선 추가하기  (0) 2019.01.26
SharedPreference  (0) 2019.01.10

SharedPreference

컴퓨터공부/안드로이드 2019. 1. 10. 21:52 Posted by 아는 개발자

애플리케이션을 종료한 이후에도 수정한 값을 유지하려면 데이터를 간단한 텍스트 파일에 써두거나 특정 데이터베이스의 형태로 저장해야 한다. 안드로이드에서는 관계형 데이터베이스로는 SQLite를, 그리고 단순히 key-value 값을 저장할 수 있는 용도인 Shared Preference를 제공한다.


Shared Preference 


애플리케이션의 설정 값처럼 데이터가 key-value의 형태이고 단순한 타입을 사용하며(boolean, int, float, long, string) 기록해야할 양이 얼마 되지 않는다면 관계형 데이터베이스를 사용하는 것보다 SharedPreference를 쓰는 것이 훨씬 편하다. SharedPreferece는 복잡하고 길고긴 쿼리문 없이 객체와 key값을 이용해 데이터를 쉽게 수정하고 가져올 수 있다.


1. 객체 생성


settings = Context.getSharedPreferences(PREF_NAME, MODE);


데이터를 수정할 수 있는 SharedPreference 객체는 Context 객체내에 내장된 getSharedPreference 함수를 통해 가져올 수 있다. 상황에 따라서 다르나 나는 주로 getApplicationContext를 사용한다.


인자 값으로는 두개를 받는데 첫번째는 PREF_NAME, SharedPreference로 저장되는 데이터베이스의 이름을 말한다. 이 값이 달라지면 서로 다른 데이터베이스 파일에 저장되게 되고 별도의 key값으로 관리된다. PREF_NAME은 storage에 XML파일의 이름으로 들어간다.


두번째 인자 값은 동작하는 형태다. 주로 0을 넣거나 MODE_PRIVATE, MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE을 혼합해서 사용한다.


2. 기록


SharedPreferences.Editor editor = settings.edit();
editor.putLong(FILE_ID, fileId);
editor.commit();


값을 기록할 때는 생성한 SharedPreference 객체로부터 editor 객체를 가져온다. 이 객체내에는 putInt, putBoolean, putString ... 함수가 있는데  모두 key값에 해당하는 데이터가 없으면 데이터를 추가하고 있으면 업데이트 하는 함수다. 저장하는 데이터 타입은 함수 명으로 유추할 수 있다.


첫번쨰 인자 값은 key값이고 두번째 인자 값은 저장할 데이터의 값이다. key값은 나중에 데이터를 읽어올 때 필요하니 static 변수로 저장해두는 것이 편하다.


putXXX 함수를 호출한 다음에는 에디터 함수 내의 commit 또는 apply함수를 호출해서 데이터 값을 최종적으로 기록한다. 호출하지 않으면 수정한 값이 파일에 저장되지 않는다. commit은 동기 방식으로 apply는 비동기 방식으로 값을 업데이트하니 이 또한 상황에 따라서 적절히 활용하면 된다.


3. 읽어오기 


return settings.getLong(FILE_ID, -1);


값을 수정할 때 처럼 SharedPreference 함수에 내장된 getXXX 함수를 통해서 읽어올 수 있다. 첫번재 인자는 읽어올 key 값이고 두번째 인자는 만약 key값에 해당하는 데이터가 없거나 읽어오는데 실패했을 때 기본으로 얻게될 데이터 값이다. 


4. 주의사항


공식문서에는 SharedPreference의 동작 형태중 하나인 MODE_MULTI_PROCESS 가 API23 이후로 Deprecated 됐고 앞으로 안드로이드에서도 지원할 생각이 없다고 한다.


MODE_MULTI_PROCESS 에 해당하는 대표적인 예로 안드로이드 UI 컴포넌트와 안드로이드 서비스가 별도의 프로세스로 분리된 경우가 있다. 개발하다보면 안드로이드 액티비티와 서비스가 데이터를 공유해야하는 일이 종종 생기는데 Context와 데이터 이름만 지정하면 얻어올 수 있는 SharedPreference 객체를 사용하는 것이 가장 심플하게 구현할 수 있는 방법이라 자주 사용된다.


액티비티와 서비스가 동일한 프로세스라면 상관 없지만 둘이 별도의 프로세스인 경우엔 실행도중에 동기화 문제가 발생한다. 글쓴이가 몸소 체험한 동기화 문제는.. 서비스를 먼저 실행시킨 다음 액티비티 에서 값을 변경했더니 백그라운드 서비스에서 수정한 값을 읽어오지 않고 예전 값을 계속 읽어오던 일이 있었다. 처음에는 데이터 수정이 제대로 되지 않았는지가 의심스러워 commit() 함수 리턴값만 확인 했었는데 알고보니 별도의 프로세스 환경에서는 사용할 수 없었다. 이것 때문에 몇시간을 잡았는지 모른다.



'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

AsyncTask  (0) 2019.03.13
ViewPager와 PageAdapter  (0) 2019.03.05
px, dp, sp 개념 정리  (0) 2019.02.16
Device screen dpi 값에 따라 처리하기  (0) 2019.02.16
안드로이드 선 추가하기  (0) 2019.01.26
SharedPreference  (0) 2019.01.10

구조설계서에 시스템에서 사용할 클래스 다이어그램을 그리다 보니 작성한 내용 그대로 실제로 시스템이 적용 할 수 있을지 의문이었다. 문서의 내용은 그럴듯 한데 여기에 안드로이드 시스템 특유의 feature들을 넣다보면 중간에 엉키게 되는 부분이 상당수 있을 것 같은 느낌이 드었다. 나중에 반복해서 고치는것 보단 직접 먼저 코드를 수정한 후 현실 가능성을 검증하고 옮기는 것이 나을것 같아 예전부터 가장 먼저 손대기로 했었던 DatabaseHelper 클래스를 리팩토링 했다.


수정전 DatabaseHelper.java 클래스는 이랬다.


그림 1. 초기 DatabaseHelper 클래스 다이어그램


DatabaseHelper 클래스는 이름 그래도 이기적인 총무의 데이터베이스를 관리할 수 있는 기능을 제공하는 클래스다. 그런데 여기에 Party, Pay, Person, Pay-Participation 테이블에 대해서 각각 CRUD 명령을 모두 넣다보니 위 클래스가 갖는 함수가 너무 많다. 클래스는 하나의 Responsibility만 가져야 하는데 위 경우에는 4개의 Responsibility를 가지게 됐으며 이런 경우 한 영역의 수정이 다른 영역의 수정에 영향을 미치게 된다. 객체지향적 관점에서 부적합한 설계 방법론이다. 위 클래스가 갖고 있는 Responsibility를 최소화하기 위해 총 4개의 subclass로 분리했다.


1. 역할에 맞춰서 클래스를 분리한다


그림 2. 1차 수정 버전


DatabaseHelper 클래스를 abstract 클래스로 두고 Table 별로 서브클래스를 두었다. 이렇게 하니 각각이 기능성으로 분류돼 각 클래스 별로 하나의 기능만 존재하게 됐다. 그런데 위 디자인을 적용해서 설치해보니 실행중 에러가 발생한다. 테이블이 생성되지 않았다는 에러다.


테이블을 생성하는 작업은 onCreate() 함수 안에서 담당한다. 그림 1에선 onCreate 함수 내에 모든 테이블을 생성하도록 했고, 그림 2에서는 각 클래스내에 존재하는 onCreate 함수에 테이블을 생성하도록 했다. 그런데 이렇게하니 문제가 발생했다. onCreate 함수는 애플리케이션내에서 데이터 베이스 함수를 열 때(getWritableDatabase 함수를 실행할 때) 딱 한번만 실행 된다. 각각이 현재 분리돼 있지만 시스템 내에서 실행되는 경우는 딱 한번 뿐이다. 그래서 PartyDatabseHelper에서 먼저 onCreate 함수를 실행 했다면 나머지 클래스에선 모두 생략된다.


위 문제는 시스템을 시작할 때 테이블을 생성하는 작업을 실행하도록 해 간단히 해결 할 수 있다. 그런데 테이블을 업그레이드 해야하는 경우는 문제가 생긴다. 업그레이드 명령인 경우 현재 데이터베이스 버전 번호와 최신 데이터베이스 버전 정보가 필요한데 이런 경우에는 기존 onUpgrade 루틴을 벗어나서 버전 정보를 읽어오는 것이 불가능하며 설사 가능하더라도 기존에 있는 기능을 중복해서 만드는것도 바람직한 코딩이 아니다.


마침 다른 블로그에 여러개의 테이블을 관리하는 코드가 있어 그 코드를 참조해서 리팩토링 했다.


2. 기존 create/upgrade 루틴과 호환되도록 한다.


그림 3. 2차 수정 버전


Party 데이터 베이스 관리 클래스가 DatabaseHelper 클래스로의 서브 클래스였는데 이제는 모두 별도의 클래스가 됐고 onCreate/onUpgrade 함수 대신 createTable(), upgradeTable()이 생겼다. 이 함수들은 static으로 선언해 외부에서도 불러줄 수 있도록 했다. DatabaseHelper에서는 기존대로 onCreate 루틴을 타는데 이때 각 서브클래스에서 선언된 createTable를 불러주도록 했다. onUpgrade 일때는 각 서브클래스에서 선언된 upgradeTable을 부른다. 


    @Override
    public void onCreate(SQLiteDatabase db) {
        PartyDatabaseManager.createTable(db);
        PayDatabaseManager.createTable(db);
        PersonDatabaseManager.createTable(db);
        PayParticipateDatabaseManager.createTable(db);

        Log.d(LOGTAG, "Create table is called");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        PartyDatabaseManager.upgradeTable(db, oldVersion, newVersion);
        PayDatabaseManager.upgradeTable(db, oldVersion, newVersion);
        PersonDatabaseManager.upgradeTable(db, oldVersion, newVersion);
        PayParticipateDatabaseManager.upgradeTable(db, oldVersion,newVersion);

        onCreate(db);
    }


기존 생성/업데이트 루틴을 타게 됐으니 앞서 말한 문제점이 깔끔하게 해결 된 것 같지만 이 코드도 문제점이 없는건 아니다. 만약 시스템에서 관리하는 테이블이 추가된다면 DatabaseHelper 클래스에 함수의 테이블 생성 작업과 업그레이드 작업을 추가해야 한다. 그런데 DatabaseHelper가 시스템 테이블의 구체적인 정보까지 알고 있는 것은 바람직하지 않다. DatabaseHelper는 각 테이블에게 추가 또는 업데이트를 명령할 뿐이지 구체적으로 어떤 테이블이 있는지는 알 필요가 없다.


차라리 외부에서 DatabaseHelper가 관리 해야하는 테이블을 등록하고 생성/업데이트 할 때 등록된 테이블만 관리하는 방법이 깔끔한 것 같다. 그래서 다음과 같이 변경했다.


3. DatabaseHelper에는 세부적인 내용을 넣지 않는다.


그림 4. 3차 수정 버전


 
// startActivity.java 내에 관리할 테이블 정보를 등록하는 코드.
        DatabaseHelper databaseHelper = new DatabaseHelper(this);

        databaseHelper.registerTableManager(new PartyTableManager());
        databaseHelper.registerTableManager(new PayJoinTableManager());
        databaseHelper.registerTableManager(new PayTableManager());
        databaseHelper.registerTableManager(new PersonTableManager());
 
// DatabaseHelper 클래스는 등록된 테이블 별로 create/update 함수를 부른다
    @Override
    public void onCreate(SQLiteDatabase db) {
        for (TableManager tableManager : tableManagerList)
            tableManager.createTable(db);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        for (TableManager tableManager : tableManagerList)
            tableManager.upgradeTable(db, oldVersion, newVersion);

        onCreate(db);
    }


TableManager라는 인터페이스를 둬 DatabaseHelper가 각 클래스의 세부 내용을 모르더라도 함수를 호출 할 수 있도록 했고 각각의 Table을 관리하는 클래스는 TableManager 인터페이스를 구현하도록 했다. DatabaeHelper 생성시 관리할 테이블을 등록해 안에 세부적으로 구현하지 않고 생성/업그레이드 작업을 수행할 수 있도록 했다. 관리할 테이블이 추가되면 startActivity에 테이블을 추가로 등록하면 되고 테이블을 삭제할 경우에는 등록하는 부분의 테이블을 지우면 된다. 이전보다 훨씬 쉽게 변경이 가능한 코드가 됐다.