관련코드 Github주소 바로가기 - https://github.com/puze/AndroidCalendar.git

목표

  • MVC패턴의 이해
  • 뷰바인딩의 사용
  • 리사이클러 뷰의 사용
  • 어댑터 안에서의 바인딩 사용
  • 캘린더 클래스의 사용

MVC 패턴

MVC패턴이란 Model - View - Controller 로 이루어진 디자인 패턴이다.
이러한 패턴을 적용 시킴으로써 다른 사람이 코드를 파악하는 것이 용이하고
유지보수를 쉽게 할수 있게 한다.

  • 모델(Model)
    • 데이터의 집합이라고 볼 수 있다.
    • 데이터의 가공까지 포함한다.
    • 모델에선 뷰와 컨트롤러의 정보를 가지고 있지 않다. (독립적임)
  • 뷰(View)
    • 사용자에게 보여지는 부분을 담당한다.
    • 뷰에선 모델의 정보를 표시하지만 컨트롤러의 정보는 가지고 있지 않다. (모델에 종속적임)
  • 컨트롤러(Controller)
    • 사용자의 입력을 받는다.
    • 화면 뒤에서 모델과 뷰를 연결하는 역할을 한다. (모델과 뷰에 종속적)

독립적 : 어떤(혹은 해당) 클래스가 없어도 작동할 수 있다.
종속적 : 작동하기위해 해당 클래스가 필요하다.

이 디자인 패턴을 이 안드로이드 아키텍처에 적용시키면 어떻게 될까.
나름대로 MVC패턴으로 앞으로 만들 캘린더를 구조화를 시켜보자면

  • 모델 : 액티비티 구성에 필요한 항목의 데이터 -> 뷰 바인딩과 엑티비티에 독립적
  • 뷰 : 메인 액티비티의 뷰 바인딩(애초에 개발자가 구현하지 않음) -> 데이터와 엑티비티에 독립적
  • 컨트롤러 : 메인 액티비티 -> 데이터와 바인딩에 종속적

뷰(바인딩)가 모델에 독립적일 필요는 없지만 위의 대전제는 성립하므로 이런 구조를 생각해볼 수 있겠다.

사실 뷰가 모델에 독립적인 상태로 MVP패턴에 가깝다.
MVC와의 차이는 컨트롤러가 Presenter가 되고 뷰는 모델에 독립적이게된다.

액티비티는 뷰로서만 사용하고 액티비티 컨트롤러 클래스를 따로 작성하는 방법도 있다.

뷰 바인딩

  1. build.gradle 추가
    build.gradle에 다음을 추가한다.
android {
...
    buildFeatures {
        viewBinding = ture
    }
}
  1. 레이아웃 xml 작성
    엑티비티에 올라갈 레이아웃을 작성한다
  2. 뷰 바인딩을 초기화

위의 코드와 같이 lateinit 으로 레이아웃의 binding을 선언한 뒤
onCreate() 에서 binding에 inflate함수로 초기화를 한다.
액티비티의 setContentView는 binding.root를 파라미터로 불러옴으로써 레이아웃 역시 초기화가 가능하다.
이후 binding 변수를 통해 해당 액티비티의 id를 선언한 모든 뷰에 접근이 가능하다.

이전의 방식으로는 findOfViewId(R.id.AAA) 식으로 접근 하거나
잠깐 레이아웃에서 설정한 id에 직접 접근(Kotlin Synthetic)이 가능했었다.
출처 : https://todaycode.tistory.com/29

리사이클러뷰

  • 리스트뷰와 그리드뷰를 대신 할 수 있는 뷰
  • 어댑터를 이용해 뷰를 컨트롤
  • 이름으로부터 알수 있듯이 재활용이 포인트
  • 재활용 하는것은 각 항목(item)
    출처 :https://blog.hexabrain.net/363
  • 리사이클러뷰를 선언한 후 레이아웃 매니저를 통해서 초기화하여 사용한다.
  • 사용가능한 레이아웃은 그리드레이아웃, 리니어 레이아웃, 스태거드 그리드 레이아웃이 있다. "레이아웃 매니저 바로가기"

어댑터

위의 MVC패턴과 유사한 아이디어에서 나온 오브젝트이다.
컨트롤러부분을 담당하기위한 오브젝트이며 뷰에서 데이터의 가공과 로직을 배제하고 어댑터에서 작성한다.
구조는 다음과 같이 되겠다.

또한 리사이클러뷰 어댑터를 구현하는데 있어서 뷰홀더 또한 구현해야한다.
뷰홀더란 리사이클러 뷰에 들어갈 항목의 뷰 및 메타데이터를 뜻한다.

캘린더 구현

  1. 레이아웃 생성
    기본적으로 사용할 메인 액티비티와 리사이클러뷰를 포함한 레이아웃을 준비한다.
  2. 레이아웃 매니저 할당
    다음과 같이 리사이클러뷰의 레이아웃 매니저에 그리드 레이아웃 매니저를 할당한다.
  3. 모델(데이터) 생성
    캘린더 안에 들어갈 아이템의 데이터 클래스를 생성한다.
    우리는 항목에 들어갈 일(day)만 표시할 것 이므로 int형 day하나만 포함한다.
  4. 뷰홀더 구현
    어댑터를 구현하는데 있어서 필요한 뷰홀더도 구현하기로한다.
    현재 구현할 뷰 홀더는 캘린더 어댑터에 종속적이기 때문에
    캘린더 어댑터 클래스안에 inner class(nested class)로 작성한다.
    생성자 파라미터로는 항목에들어갈 레이아웃의 바인딩을 받아와
    init생성자에 해당 항목에 클릭리스너를 달아준다.
    또한 bind메소드를 구현하여 해당 항목이 어떻게 표시될지 작성을한다.
    파라미터로는 앞서 작성했던 모델을 사용하도록 한다.
  5. 어댑터 구현
    리사이클러 뷰의 어댑터를 상속받고 (RecyclerView.Adapter<CalendarAdapter.CalendarViewHolder>())
    오버라이딩이 필요한 메소드는 3개가 있다.
    • onCreateViewHolder : 뷰홀더를 생성할때 호출되며 여기서 생성한 뷰홀더 객체를 반환한다.
    • onBindViewHolder : 뷰홀더를 실제 데이터와 연결한다.
    • getItemCount : 관리해야할 아이템의 크기를 반환한다.
  6. 액티비티 구현
    리사이클러뷰의 구현에 필요한 준비는 끝났고 안에 담을 내용을 구현하자.
    캘린더를 구현하기 위해 Calendar클래스를 이용한다.
    이 클래스를 이용해 직접 구현하고 다루려면 힘든 달력관련 함수들을 손쉽게 다룰수 있다.
    캘린더 작성 알고리즘은 다음과 같다.
    • 캘린더를 현재 달의 1일로 설정한다.
    • 리사이클러 뷰에 들어갈 데이터리스트를 선언한다.
    • 1일이 시작하는 요일을 찾고 월요일부터 해당요일의 차를 빈 데이터로 넣는다.
    • 해당 월의 1일부터 해당 월의 마지막 일까지 넣는다.
    • 작업이 끝난 데이터리스트를 이용하여 어댑터를 생성하고 리사이클러 뷰의 어댑터에 할당한다.캘린더 클래스를 이용하여 현재달 역시 출력 할 수 있다.

확장 가능한 부분

  • 다른 날짜를 선택하여 해당 캘린더를 다시 그리는 기능
  • 액티비티에 종속되있지 않고 어디서나 불러올수 있게 모듈화 구현
  • 뷰홀더에 일 이외에 다른 데이터(메모 혹은 스케줄)도 표시

마치며..

캘린더를 작성하기위해 mvc패턴과 뷰바인더, 리사이클러뷰, 어댑터, 뷰홀더까지 여러 개념들을 소개하였다.
코드는 github(바로가기)에서 확인 가능하다.

1. 서론

액션바를 구현하기위해 공식문서 https://developer.android.com/training/appbar/setting-up 와

 

앱 바 설정하기  |  Android 개발자  |  Android Developers

가장 기본적인 형태의 작업 모음은 한쪽에는 활동 제목을 표시하고 다른 쪽에는 더보기 메뉴를 표시합니다. 앱 바는 이렇게 간단한 형태로도 유용한 정보를 사용자에게 제공하고 일관된 디자인

developer.android.com

https://recipes4dev.tistory.com/149 블로그를 참고해 만들어 보기 시작했다.

 

안드로이드 툴바 기본 사용법. (Android Toolbar)

1. 안드로이드 툴바(Toolbar)와 앱바(App Bar) 툴바(Toolbar)는 안드로이드 5.0 (API Level 21)부터 추가된 위젯(Widget)으로, 앱에서 가장 중요한 액션 또는 가장 자주 사용되는 액션들을 제공하는 앱바(AppBar)..

recipes4dev.tistory.com

위에서 소개된 도구(android.support.v7.widget.Toolbar)를 사용하기위해 의존성에

implementation 'com.android.support:appcompat-v7:27.1.1'를 추가해야하는데 실제로 추가해보면

Version 28 (intended for Android Pie and below) is the last version of the legacy support library, so we recommend that you migrate to AndroidX libraries when using Android Q and moving forward. The IDE can help with this: Refactor > Migrate to AndroidX...

라는 오류메시지를 뿜게된다. 말머리에 굳이 안드로이드 스튜디오 버전을 적은 것은 버전업에 따라 마이그레이션이 필요해지고 정보를 찾아 해매게 되어 적게 된 것이다.

마이그레이트가 필요하다니 안드로이드 스튜디오에서 원하는대로 해보자.

 

2. 구현

커스텀 액션바를 만들기 위해 위에서 언급한 액션바를 추가할 xml 파일에 <android.support.v7.widget.Toolbar> 대신 <com.google.android.material.appbar.MaterialToolbar>를 사용하도록 한다. 그리고 내 마음대로 커스텀을 하면된다.

예를 들어 가운데 정렬을 하기위해선 다음과같이 app:titlecentered="true"를 지정한다.

1
2
3
4
5
6
7
8
9
10
11
    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/app_actionbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:theme="?attr/actionBarTheme"
        app:title="Action Bar"
        app:titleCentered="true"
        app:titleTextColor="@color/black"
        android:elevation="4dp">
    </com.google.android.material.appbar.MaterialToolbar>
cs

위와같은 방법으로 정렬을 하지 않고 툴바 태그안에 레이아웃을 넣고 정렬을 하면 네비게이션 아이콘이나 메뉴 아이콘에 영향을 받게된다.

 

시험삼아 메뉴 아이콘을 추가해보자.

액션바에 추가할 메뉴 xml을 작성한다. (res/menu/)

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/test_action1"
        android:title="test1"
        app:showAsAction="never"/>
    <item
        android:id="@+id/test_action2"
        android:title="test2"
        app:showAsAction="never" />
</menu>
cs

액션바를 추가할 Activity에 (AppCompatActivity를 상속받은 Activity) 다음과 같은 onCreateOptionsMenu를 오버라이딩 해준 후 위에서 준비한 메뉴 xml 파일을 inflate 해준다.

1
2
3
4
5
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.actionbar_actions, menu);
        return true;
    }
cs

 

 

완성된 커스텀 액션바

 

 

참고:https://github.com/material-components/material-components-android/issues/2011

 

[Toolbar] How to center title of MaterialToolbar? · Issue #2011 · material-components/material-components-android

I am trying to center the title of the MaterialToolbar, tried to add android: gravity but it doesn't work. There is no info in the documentation on how to center it <androidx.coordinatorlayo...< p=""> </androidx.coordinatorlayo...<>

github.com

 

1
2
int pxValue = 30;
int dpValue = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, pxValue , mainContext.getResources().getDisplayMetrics());
cs

1. 이미지를 Bitmap으로 변환

1. 안드로이드 리소스를 Bitmap으로 변환

1
2
// drawable에 있는 리소스를 BitmapFactory를 이용해 bitmap 작성
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_guest, options);
cs

 

2. 파일을 Bitmap으로 변환

1
2
3
4
// File의 이미지를 BitmapFactory를 이용해 Bitmap 작성
File file = new File(filePath, fileName);
FileInputStream fileInputStream = new FileInputStream(file);
Bitmap bitmap = BitmapFactory.decodeFile(filePath + "/" + fileName);
cs

 

인터넷에서 이미지 다운로드는 이쪽으로 : https://puzi.tistory.com/31

 

[안드로이드]이미지 다운로드

메인 쓰레드는 이미지 다운로드 동안 기다릴 수 없기 때문에 AsynkTask로 작성한다. 그리고 리스너를 달아주어 다운로드 완료 후 처리를 하도록한다. ImageUrlDown.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1..

puzi.tistory.com

 

2. Bitamp을 byte로 저장 후 Base64로 encoding

1
2
3
4
5
6
// BytArrayOutputStream을 이용해 Bitmap 인코딩
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
// 인코딩된 ByteStream을 String으로 획득
byte[] image = byteArrayOutputStream.toByteArray();
String byteStream = Base64.encodeToString(image, 0);
cs

 

3. html의 자바스크립트를 호출하여 Base64를 넘겨줌

1
2
3
// ByteStream을 자바스크립트를 이용해 전달
binding.mainWebView.loadUrl("javascript:setImageByteCode('data:image/png;base64," + byteStream + "')");
 
cs

 

4. 넘겨받은 Base64값으로 이미지 출력

<HTML>

1
2
3
4
5
6
7
8
9
10
    <head>
        <script id="applicationScript">
            function setImageByteCode(byteCode) {
                document.getElementById("photo").src = byteCode;
            }
        </script>
    </head>
    <body>
        <img id = "photo" src = "trans.png" title="picture">        
    </body>
cs

 

메인 쓰레드는 이미지 다운로드 동안 기다릴 수 없기 때문에 AsynkTask로 작성한다.

그리고 리스너를 달아주어 다운로드 완료 후 처리를 하도록한다.

 

ImageUrlDown.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.example.test_html_bytecode;
 
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
 
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
 
public class ImageUrlDown extends AsyncTask<StringString, Bitmap> {
    // 다운로드 완료 후 호출 할 리스너
    public interface OnPostDownLoadListener {
        void onPost(Bitmap bitmap);
    }
 
    private Bitmap bitmap = null;
    private OnPostDownLoadListener onPostDownLoad;
 
    // 리스너 세팅
    public ImageUrlDown(OnPostDownLoadListener paramOnPostDownLad) {
        onPostDownLoad = paramOnPostDownLad;
    }
    
    @Override
    protected Bitmap doInBackground(String... strings) {
        try {
            // 파라미터로 받은 url로 부터 이미지 다운로드
            bitmap = BitmapFactory.decodeStream((InputStream) new URL(strings[0]).getContent());
            return bitmap;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
 
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        // 이미지 다운로드 완료 후 리스너 호출
        if (onPostDownLoad != null)
            onPostDownLoad.onPost(bitmap);
    }
}
 
cs

 

호출은 아래와 같이한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    private void downloadImage() {
        String imageURL = "http://puzi.tistory.com/image.png"//이미지 URL
        // 이미지 다운로드 클래스 생성 및 리스너 작성
        ImageUrlDown imageUrlDown = new ImageUrlDown((bitmap) -> {
            try {
                // 파일 생성
                File file = new File(filePath,fileName);
                // 아웃풋 스트림 생성
                FileOutputStream fileOutputStream = new FileOutputStream(file);
                // 아웃풋 스트림 작성
                bitmap.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
                // 아웃풋 스트림 출력
                fileOutputStream.flush();
                // 아웃풋 스트림 종료
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        // url 전달
        imageUrlDown.execute(imageURL);
    }
cs

참고 : https://lannstark.tistory.com/34?category=840464 

 

Java InputStream이란?

InputStream OutputStream을 실무에서 사용할 때면, 뭔가 알긴 알고 실제로 둘을 활용해 기능을 구현하는데는 전혀 문제가 없지만, 사용할때마다 찾아보게되고 뭔가 정확히 아는 것 같지는 않다라는 느

lannstark.tistory.com

 

1. 개요

안드로이드 스튜디오에서 제공하는 navigation drawer에 추가한 메뉴들은 기본적으로 테마의 영향을 받으며 색상변경이 자유롭게 되지 않는다.

각 아이콘의 색을 변경 할 필요가 있을 때 해결 방법이다.

그림1. Navigation drawer (출처:https://developer.android.com/guide/navigation/navigation-ui)

2. 해결 방법

다음의 한줄로 해결이 가능하다.

findViewById(R.id.nav_view).setItemIconTintList(null);

위의 코드를 onCreate()에 추가 한 후 다음 과 같이 색이 다른 이미지로 아이콘을 추가하면 색상이 살아나게 된다.

1
2
3
4
5
6
int[] icons = new int[]{R.drawable.ic_disk1, R.drawable.ic_disk2, R.drawable.ic_disk3};            
for (index = 0; index < users.size(); index++) {
    UserInfo userinfo = users[index];
    MenuItem user = menu.add(R.id.user_menu, Menu.NONE, index, userInfo.id);
    user.setIcon(icons[index % 3]);
}
cs

 

 

그림2. Navigation drawer에 색상이 반영된 icon

 

출처 : https://stackoverflow.com/questions/33407448/change-color-of-navigation-drawer-icon-in-android-studio-default-template/62075034#62075034?newreg=a157ba9e1bdc4cdf943858021f8d66c6 

 

Change color of Navigation Drawer Icon in Android Studio default template

The new default Navigation Drawer Activity template in Android Studio defines its titles and icons in a menu file activity_main_drawer like this: ...

stackoverflow.com

 

1. 목적

한번 클릭에 상태가 유지 되고 다시 한번 클릭에 상태가 해제되는 토글 버튼을 만들고싶다.

 

2. 요구사항

1. 배경이보이고 이미지가 있는 뷰를 사용한다.

2. 이미지 크기 이상의 버튼 영역이 필요로 한다.

3. 토글 될때 마다 이미지가 변하도록 한다.

4. 버튼이 눌리는 효과를 주도록 한다. (배경색이 변하도록 한다)

 

3. 구현

1. 이미지 버튼을 준비한다

1
2
3
4
5
6
    <ImageButton
        android:id="@+id/test_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/baseline_favorite_border_24"
        />
cs

 

 

2. 설정영역에 알맞게 뷰의 크기를 조정한다.

1
2
3
4
5
6
    <ImageButton
        android:id="@+id/test_button"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/baseline_favorite_border_24"
        />
cs

 

3. 버튼의 리스너를 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
        ImageButton button = findViewById(R.id.test_button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(button.isSelected()) {
                    button.setImageResource(R.drawable.baseline_favorite_border_24);
                } else {
                    button.setImageResource(R.drawable.baseline_favorite_24);
                }
                button.setSelected(!button.isSelected());
            }
        });
cs

 

4. 클릭시 효과를 주기위해 백그라운를 정의한다. (ripple효과)

1
2
3
4
5
6
7
    <ImageButton
        android:id="@+id/test_button"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/baseline_favorite_border_24"
        android:background="?attr/selectableItemBackground"
        />
cs

1. 서론

안드로이드 스튜디오로 연속 클릭을 감지하는 메서드를 구현하려고 한다.

유니티는 Update() 함수를 지원하여 프레임과 시간을 조작하지만

안드로이드 스튜디오에선 어떻게 구현할지 생각해 보았다.

 

2. 설계

시스템 시간을 이용해 마지막으로 클릭된 시간을 저장 한후 현재시간과 비교하는 방법으로 구현하려한다.

 

3. 구현

 

 일단 클릭 감지를 하기 위해 onCreate()에 리스너를 달아주었다.

1
findViewById(R.id.button).setOnClickListener(v -> TouchContinuously());
cs

 

다음은 연속 클릭을 감지하는 메소드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    private long lastClickTime = 0// 마지막 클릭 시간
    private int clickTime = 0// 클릭 된 횟수
    private final int TIMES_REQUIRED = 5// 총 필요한 클릭 횟수
    private final int TIME_TIMEOUT = 2000;  // 마지막 클릭후 제한시간
 
    private void TouchContinuously() {
        if (SystemClock.elapsedRealtime() - lastClickTime < TIME_TIMEOUT) {
            clickTime++;
        } else {
            clickTime = 1;
        }
        lastClickTime = SystemClock.elapsedRealtime();
 
        if (clickTime == TIMES_REQUIRED) {
            // TODO 연속 클릭 완료 후 메소드 구현
            Toast.makeText(this"연속 클릭 완료", Toast.LENGTH_SHORT).show();
        }
    }
cs

현재시간 (SystemClock.elapsedRealtime())과 마지막으로 클릭 된 시간(lastClickTime) 의 차이가 제한시간( TIME_TIMEOUT)을 넘는다면 클릭 된 횟수를 1로 초기화하고 넘지않는 다면 클릭 된 횟수를 1회 증가시킨다.

 

그리고 클릭 된 횟수가 필요한 클릭 횟수를 도달 하였을 시 원하는 메소드를 호출 하도록 한다.

 

참고 :https://threeidiotscoding.tistory.com/18

 

[Android Studio] 간단하게 버튼 중복클릭 시간 제한하기

[Android Studio] 간단하게 버튼 중복클릭 시간 제한하기 안드로이드 스튜디오에서 onClick 이벤트가 일어나는 버튼이나 이미지, 텍스트 뷰와 같은 곳에 중복클릭이 일어나지않도록 조건을 두려

threeidiotscoding.tistory.com

 

+ Recent posts