유니티를 처음 시작하여 사용법을 잘 모르더라도 코딩을 하지 않아도 간단한 몇개의 클릭만으로 AR을 테스트하는 방법을 알아봅니다.

진짜 간단한 테스트 이므로 너무 기대는 하지 말고 방법만 알아보고 천천히 살을 붙여 나가다 보면 좋은 프로그램이 되어 있겠죠.

Unity 설치와 관련해서는 전에 설명을 하였는데(https://copycoding.tistory.com/446) 복습 차원에서 먼저 Unity 버전을 새로 하나 Hub에 설치하고 진행을 하도록 하겠습니다.

 

 

유니티 허브를 실행하고 좌측 메뉴에서 [Installs]를 선택해 주고 우측 상단에 있는 [Install Editor] 버튼을 클릭하여 설치 창을 오픈합니다.

 

 

 

Install Unity Editor 창이 나타나면 상단 탭에서 [Archive]를 선택합니다.

[Archive] 탭에서 하단 안내글이 있는데 여기서 파란색 [download archive]를 클릭해 주면 웹 브라우저 창이 나타납니다.

 

 

 

 

상단에 연도별 버전 탭이 있고 하단에 상세 버전의 목록이 있습니다.

본인이 사용하려는 버전이 있다면 찾아서 설치를 진행하면 됩니다.

여기서는 테스트용으로 Unity 2022.3.11을 설치해 보도록 하겠습니다.

버전 앞에 있는 파란색 [Unity Hub] 버튼을 클릭합니다.

 

 

 

웹에서 선택한 버전을 유니티 허브 프로그램으로 연결하여 설치를 진행한다는 파업이 나오는데 [열기] 버튼을 클릭해줍니다.

 

 

 

Unity Hub 프로그램에 웹에서 선택한 버전이 들어왔습니다.

각 버전별로 지원하는 추가적인 프로그램들이 있는데 스크롤을 하면서 원하는 기능을 선택해서 설치를 진행하면 됩니다.

 

기본적으로 선택되어있는 Microsoft Visual Studio Community 2022는 물체들을 움직임과 기능을 부여하기 위한 프로그램 작성에 사용됩니다.  지금 사용을 하지 않겠지만 설치하는 버전을 이용하여 Object에 기능을 부여하려면 필요하므로 설치 과정을 한번 보는 용도로 체크했습니다.

 

 

 

안드로이드 프로그램을 위해 Android Build Support도 클릭하고(아이폰 있으신분은 iOS 클릭)

하단으로 스크롤해서 필요한 기능들이 더 있으면 선택하고

 

 

 

마지막으로 [Continue]버튼을 클릭합니다. 

프로그램이 난이도가 올라가면 계속해서 플러그인등을 추가하게 되겠죠.

저는 맨 마지막 한국어 선택은 실제 하지 않았습니다.  모르는 툴을 학습할때는 한글로 설명해주는 사이트만 찾아보기에는 한계가 있어서 불편할 때가 있습니다.

 

Continue 버튼을 클릭하면 선택한 횟수만큼 설치를 위해 선택한 프로그램들의 라이선스 동의를 위한 팝업들이 나옵니다.

, 개인에 따라 어떤 프로그램을 체크했는지에 따라 여기에서 보이는 팝업들과 다르게 나옵니다.

 

 

 

Visual Studio 라이센스 확인하고 [Continue] 버튼을 클릭합니다.

다른 라이센스 확인용 팝업이 나오면 처크하고 [Continue] 버튼을 클릭 합니다.

 

 

 

[Install] 버튼이 보이는걸 보니 이제 마지막이네요.

Android build용 라이선스 확인하고 [Install] 버튼을 클릭하여 선택한 프로그램을 설치합니다.

 

 

 

설치 상태를 확장하면 선택한 프로그램의 상세 내역에 대해 하나씩 설치가 얼마만큼 진행되는지 확인할 수 있습니다.

 

 

 

Visual Studio CommunityInstaller 설치가 완료되면 실제 프로그램을 선택해서 설치해야 합니다.

스크롤을 하단으로 죽 내리다 보면 [Unity를 사용한 게임 개발]이 보이는데 이것을 체크합니다.

 

 

 

그리고 우측의 세부 정보 중에 Unity Hub의 선택은 제외합니다.

이미 설치를 했으니 다시 설치할 필요는 없겠지요.

우측 하단 [설치] 버튼을 클릭하여 설치작업을 진행합니다.

 

 

 

 

설치가 완료되었습니다. [OK] 버튼을 클릭해 줍니다.

 

 

 

 

창을 닫고 다시 Unity Hub 로 돌아와 모든 프로그램의 설치를 기다립니다.

 

 

 

설치가 완료되었습니다.

 

 

 

프로그램이 설치된 위치를 확인할 수 있습니다.

이제 새로운 프로젝트를 생성해 보겠습니다.

 

 

 

좌측 메뉴에서 [Projects]를 선택하고 우측 상단의 [New project] 버튼을 클릭합니다.

 

 

 

기본적인 템플릿을 제공하여 쉽게 2차원, 3차원 게임은 물론 가상환경등의 환경을 구축할 수 있습니다.

AR을 선택하고 프로젝트명과 저장 위치(기본 설정이 맘에 안들면 임의로 생성합니다)를 선택하고 [Create project] 버튼을 클릭합니다.

 

선택된 값으로 개발 환경이 생성됩니다.

먼저 Unity Hub 를 살펴보면

 

 

 

설정한 폴더에 입력한 명칭으로 프로젝트가 생성이 되었습니다.

 

 

 

그리고 작업 환경이 생성됩니다.

 

 

 

작업을 할 수 있는 Unity Editor 화면이 생성되었습니다.

 

여기서부터 실제 작업이 시작됩니다.  그렇다고 복잡한 건 아니고 Android 또는 iOS용 선택을 몇개 하는 것이 전부입니다.

 

이제 AR에 필요한 package가 설치되어 있는지 확인해 봅니다.

 

 

 

Window > Package Manager 메뉴를 선택하면 팝업 창이 나옵니다.

 

 

 

Package Manager 팝업 창이 버전에 따라 조금 모양이 다를 수 있는데 일단 리스트에서 AR Foundation, ARCore 가 설치되어 있는지 확인합니다.  iOS용을 만든다면 ARKit를 설치합니다.

만약 설치가 되어있지 않다면 우측의 [Install] 버튼을 이용하여(Remove 버튼 위치) 설치합니다.

 

다음은 현재 프로젝트를 어느 플랫폼에 빌드할지 설정합니다.

 

 

 

File > Build Settings… 메뉴를 선택해서 팝업 창을 오픈 합니다.

 

 

 

저는 아이폰이 없으므로 좌측 platform에서 [Android]를 선택하고

우측 하단의 [Switch Platform] 버튼을 클릭하여 프로젝트가 안드로이드에서 실행되기위한 파일을 import 시킵니다.

 

 

 

임포트가 완료되어 가는군요.

 

 

 

[Switch Platform] 버튼이 [Build] 버튼으로 변경되었습니다.

Unity 프로그램을 android에서 돌리기 위한 프로그램은 설치가 완료되었습니다.

이제 안드로이드 기기에 대한 상세 설정을 하기위해 [Player Settings…] 버튼을 클릭하여 팝업을 오픈 합니다.

 

 

 

상세하게 설정을 하기 위해서는 좌측 메뉴에서 Player를 선택하여 수정을 해야 하는데 여기서는 간단한 데모라 Player는 수정하지 않습니다.

마지막 메뉴인 [XR Plug-in Management]를 선택하여 우측 화면에서 안드로이드 아이콘 탭을 선택합니다.

그리고 프로젝트를 Play 하기위한 플러그인으로 첫번째 [Google ARCore]를 선택해 줍니다.

이것으로 모든 선택은 완료가 되었습니다.

특별한거 없이 그냥 안드로이드만 선택을 하면 됩니다.

 

이제 무엇을 보여줄지 화면 작업을 진행합니다.

물론 여기까지만 진행하고 실행해도 되는데 현재는 아무것도 없는 빈 공간이므로 실행해도 아무것도 보이지 않으니 아무거라도 하나 만들어야겠죠.

 

 

 

Hierarchy 창에 마우스를 놓고 우클릭을 합니다.  그리고 아무거나 3D 아이템을 하나 추가해 봅니다. 물론 다른걸 선택해도 되는데 그냥 간단한 테스트용이라 성의없이 작업합니다.

 

3D Object > Tree를 선택해 보았습니다.

 

 

 

가지 없는 나무 기둥 모양이 덩그러니 나타납니다.

나무를 클릭하고 나무의 위치를 원점에서 조금 이동을 시켰 봅니다. 원점은 내가 서있는 곳으로 나무가 같이 있으면 보이지 않겠죠.

 

 

 

나무를 하나 만들고 위치를 이동하였으니 앱에서 어떻게 보일지 확인하기 위해 상단에 있는 [Play] 버튼을 이용하여 미리보기를 해 봅니다.

 

 

 

위치를 조절 합니다.

 

대충 위치를 잡았으면 핸드폰을 연결해봅니다.

File > Build Settings… 메뉴를 클릭해서 조금전에 설정한 팝업을 띄웁니다.

 

 

 

우측에서 Run Device 항목을 찾고 [Refresh] 버튼을 클릭해서 핸드폰을 찾도록 합니다.

그리고 [Default device]를 확장하여 연결된 핸드폰을 선택하고 [Build and Run] 버튼을 클릭합니다.

그러면 apk 파일을 생성할 위치를 선택하라고 합니다.

 

 

 

관리가 쉽도록 현재 프로젝트의 폴더 하부에 새로운 폴더를 하나 생성하고(여기서는 Builds로 생성) apk 파일명도 적당하게 적어주고 [저장] 버튼을 클릭합니다.

 

 

apk 파일이 생성됩니다.

컴퓨터의 성능에 따라 시간이 많이 소요될 수도 있습니다.

잠시 후에 핸드폰으로 생성된 증강현실 화면의 확인이 가능합니다.

저는 핸드폰이 옛날거라 그런지 처음에는 까만 화면만 나와서 핸드폰에서 앱 실행을 종료하고 다시 실행했더니 잘 나오더군요.

 

 

- copy coding -

 

 

지난번에 Android에서 OpenCV를 사용하기 위한 SDK 추가작업을 진행 했었는데 실제로 사용법을 익혀 보도록 하겠습니다.

Canny 함수를 이용해서 외각선 검출을 진행하게 되며 결과는 아래와 같습니다.

 

 

대부분 이미지 작업을 하게되기 때문에 OpenCV에서 카메라 제어를 할 수 있도록 작업을 하면 나머지는 간단하게 함수 호출만 해주면 되는데 카메라에서 이미지를 받아오는 설정 부분이 버전마다 조금씩 달라서 Canny() 프로그램 함수는 한줄이면 되는데 환경 설정하는 부분에 시간이 많이 소요 될 가능성도 있습니다.

 

먼저 카메라를 사용 할 수 있도록 권한을 추가합니다.

 

AndroidManifest.xml 

<uses-permission android:name="android.permission.CAMERA" />
<
uses-feature android:name="android.hardware.camera" android:required="false" />
<
uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<
uses-feature android:name="android.hardware.camera.front" android:required="false" />
<
uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false" />

  

 

Activity_main.xml은 내용을 전부 삭제하고 OpenCV 카메라 VIew를 추가해 줍니다. 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
   
xmlns:android="http://schemas.android.com/apk/res/android"
   
xmlns:opencv="http://schemas.android.com/apk/res-auto"
   
xmlns:tools="http://schemas.android.com/tools"
   
android:layout_width="match_parent"
   
android:layout_height="match_parent"
   
android:orientation="vertical"
   
tools:context=".MainActivity">

        <
org.opencv.android.JavaCameraView
           
android:layout_width="match_parent"
           
android:layout_height="match_parent"
           
android:visibility="gone"
           
android:id="@+id/frame_Surface"
           
opencv:show_fps="true"
           
opencv:camera_id="any">

        </
org.opencv.android.JavaCameraView>
</
LinearLayout>

 

 

 

MainActivity.java에 카메라 제어 관련 소스를 추가해 줍니다.

 

Global 변수를 만들어주고 카메라로 부터 Callback을 받아 처리하면 되는 간단한 내용인데 여기서 버전마다 조금씩 다르게 작동을 해서 몇가지 추가적인 작업이 필요할 수도 있습니다.

 

먼저 변수와 callback을 선언 합니다.

private Mat mRgba;
private CameraBridgeViewBase mOpenCvCameraView;
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
   
@Override
   
public void onManagerConnected(int status) {
       
switch (status) {
           
case LoaderCallbackInterface.SUCCESS: {
                Log.d(
Tag_Log, "OpenCv loaded");
               
mOpenCvCameraView.enableView();
            }
           
break;
           
default:{
               
super.onManagerConnected(status);
            }
           
break;
        }
    }
};

 

 

onCreate()에는 화면구성시 idframe_Surface로 추가한 JavaCameraView를 설정 합니다.

 

mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.frame_Surface);
mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);
mOpenCvCameraView.setCvCameraViewListener(this);
mOpenCvCameraView.setCameraIndex(0);

  

ActivityCvCameraViewListener2를 상속해주고 이와 관련된 OpenCV 카메라 관련 메소드 3개를 Override로 추가합니다.

 

@Override
   
public void onCameraViewStarted(int width, int height) {


    }

   
@Override
   
public void onCameraViewStopped() {
    }

   
@Override
   
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
       
mRgba = inputFrame.rgba();

       
return mRgba;
    }

 

 

그리고 onCameraFrame()에서 받아온 이미지를 리턴 하면 핸드폰 화면에 이미지가 나오는데 그렇지 않은 경우 아래와 같은 기능을 추가해 줍니다.

 

@Override
protected List<? extends CameraBridgeViewBase> getCameraViewList() {
   
return Collections.singletonList(mOpenCvCameraView);
}

 

 

최신 SDK중 이렇게 getCameraViewList()Override 할 수 있도록 되는 SDK 버전이 있습니다.

 

만약 getCameraViewList()Override로 지원 안되는 SDK 버전이라면

 

Permission에서 함수를 호출 하도록 해야 합니다.

 

@Override
    @TargetApi
(Build.VERSION_CODES.M)
   
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
       
if (requestCode == CAMERA_PERMISSION_REQUEST_CODE && grantResults.length > 0
               
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            onCameraPermissionGranted();
        }
else{
           
       
}
       
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

 

  

protected void onCameraPermissionGranted() {
    List<?
extends CameraBridgeViewBase> cameraViews = getCameraViewList();
   
if (cameraViews == null) {
       
return;
    }
   
for (CameraBridgeViewBase cameraBridgeViewBase: cameraViews) {
       
if (cameraBridgeViewBase != null) {
            cameraBridgeViewBase.setCameraPermissionGranted();
        }
    }
}

 

 

여기까지 왔으면

 

 

 

 

 

이런식으로 화면이 나와야 합니다.

 

 

그럼 Canny 함수 하나를 추가해서 테스트를 진행해 봅니다.

 

@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
   
mRgba = inputFrame.rgba();

   
if(mRgba == null) {
        Log.d(
Tag_Log, "onCameraFrame error");

    }
   
Mat edges = new Mat();
    Imgproc.Canny(mRgba, edges, 80,200);


   
return edges;
}

 

  

카메라로 가져온 이미지를 OpenCV Canny()함수에 추가해서 작업하는 방법입니다.

python으로도 간단하게 함수만 호출하면 테스트 할 수 있는것과 같이 android java에서도 함수만 호출하면 됩니다.

 

 

이미지가 90도 회전되어 보이는 부분은 AndroidManifest.xml에 설정을 추가하면 되고

MainActivity.java에는 onStart(), onDestroy() 등을 추가해줍니다.

그러면 처음에 보았던 이미지를 확인할 수 있습니다.

 

AndroidManifest.xml 전체 소스 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   
package="copycoding.tistory.opencvlib">

    <
uses-permission android:name="android.permission.CAMERA" />
    <
uses-feature android:name="android.hardware.camera" android:required="false" />
    <
uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
    <
uses-feature android:name="android.hardware.camera.front" android:required="false" />
    <
uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false" />

    <
application
       
android:allowBackup="true"
       
android:icon="@mipmap/ic_launcher"
       
android:label="@string/app_name"
       
android:roundIcon="@mipmap/ic_launcher_round"
       
android:supportsRtl="true"
       
android:theme="@style/Theme.OpencvLib">
        <
activity android:name=".MainActivity"
           
android:screenOrientation="landscape"
           
android:configChanges="keyboardHidden|orientation">
            <
intent-filter>
                <
action android:name="android.intent.action.MAIN" />

                <
category android:name="android.intent.category.LAUNCHER" />
            </
intent-filter>
        </
activity>
    </
application>

</
manifest>

 

 

 

MainActivity.java 전체 소스입니다. 

package copycoding.tistory.opencvlib;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.annotation.TargetApi;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceView;
import android.view.Window;
import android.view.WindowManager;

import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;

import java.util.Collections;
import java.util.List;

import static android.Manifest.permission.CAMERA;

public class MainActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener2 {

   
private static String Tag_Log = "OpenCV Test : ---------------------";
   
private static final int CAMERA_PERMISSION_REQUEST_CODE = 200;

   
private Mat mRgba;
   
private Mat mGrey;
   
private CameraBridgeViewBase mOpenCvCameraView;
   
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
       
@Override
       
public void onManagerConnected(int status) {
           
switch (status) {
               
case LoaderCallbackInterface.SUCCESS: {
                    Log.d(
Tag_Log, "OpenCv loaded");
                   
mOpenCvCameraView.enableView();
                }
               
break;
               
default:{
                   
super.onManagerConnected(status);
                }
                
break;
            }
        }
    };


   
@Override
   
protected void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);

        requestWindowFeature(Window.
FEATURE_NO_TITLE);
        getSupportActionBar().hide();

       
setContentView(R.layout.activity_main);

       
mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.frame_Surface);
       
mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);
       
mOpenCvCameraView.setCvCameraViewListener(this);
       
mOpenCvCameraView.setCameraIndex(0);


   
}

   
@Override
   
public void onCameraViewStarted(int width, int height) {

   
}

   
@Override
   
public void onCameraViewStopped() {
    }

   
@Override
   
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
       
mRgba = inputFrame.rgba();
        if(mRgba == null) {
            Log.d(
Tag_Log, "onCameraFrame error");

        }
        Mat edges = new Mat();
        Imgproc.Canny(mRgba, edges, 80,200);


        
return edges;
    }

   
@Override
   
protected void onResume() {
       
super.onResume();
       
if(OpenCVLoader.initDebug()) {
            Log.d(
Tag_Log, "Opencv initialized");
           
mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
        }
else {
            Log.d(
Tag_Log, "Opencv not loaded");
            OpenCVLoader.initAsync(OpenCVLoader.
OPENCV_VERSION, this, mLoaderCallback);
        }
    }

   
@Override
   
protected void onPause() {
       
super.onPause();
       
if(mOpenCvCameraView != null) {
           
mOpenCvCameraView.disableView();
        }
    }

   
@Override
   
protected void onDestroy() {
       
super.onDestroy();
       
if(mOpenCvCameraView != null) {
           
mOpenCvCameraView.disableView();
        }
    }

   
@Override
   
protected void onStart() {
       
super.onStart();

       
boolean havePermission = true;
       
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
           
if (checkSelfPermission(CAMERA) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(
new String[]{CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
                havePermission =
false;
            }
        }
       
if (havePermission) {
            onCameraPermissionGranted();
        }
    }

   
protected List<? extends CameraBridgeViewBase> getCameraViewList() {
       
return Collections.singletonList(mOpenCvCameraView);
    }

   
@Override
    @TargetApi
(Build.VERSION_CODES.M)
   
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
       
if (requestCode == CAMERA_PERMISSION_REQUEST_CODE && grantResults.length > 0
               
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            onCameraPermissionGranted();
        }
else{
           
       
}
       
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }


   
protected void onCameraPermissionGranted() {
        List<?
extends CameraBridgeViewBase> cameraViews = getCameraViewList();
       
if (cameraViews == null) {
           
return;
        }
       
for (CameraBridgeViewBase cameraBridgeViewBase: cameraViews) {
           
if (cameraBridgeViewBase != null) {
                cameraBridgeViewBase.setCameraPermissionGranted();
            }
        }
    }

}

 

 

  

이전에 올렸던 OpenCV SDK 설치관련 글을 읽으러 가지 않으려면 아래 gradle을 참고 하세요.

 

plugins {
   
id
'com.android.application'
}

android {
   
compileSdkVersion
29
   
buildToolsVersion "29.0.0"

   
defaultConfig {
       
applicationId
"copycoding.tistory.opencvlib"
       
minSdkVersion 26
       
targetSdkVersion 29
       
versionCode 1
       
versionName "1.0"

       
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   
}

   
buildTypes {
       
release {
           
minifyEnabled
false
           
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       
}
    }
   
compileOptions {
       
sourceCompatibility JavaVersion.
VERSION_1_8
       
targetCompatibility JavaVersion.VERSION_1_8
   
}
}

dependencies {

   
implementation
'androidx.appcompat:appcompat:1.3.1'
   
implementation 'com.google.android.material:material:1.4.0'
   
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
   
implementation project(path: ':OpenCV')
    testImplementation
'junit:junit:4.+'
   
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
   
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

 

 

 - copy coding -

 

App에서 주기적이며 반복적인 작업이 필요한 경우가 있습니다.  화면을 주기적으로 갱신 하던가 서버와 데이터를 일정시간마다 반복적으로 보내고 받아야 하는 경우등에는 Timer를 사용하는것이 소스가 간단해서 무척 편합니다.  한가지 단점은 중지 시켰다 다시 시작하는 기능이 없기 때문에 만일 화면에서 버튼으로 제어 하려면 제어를 한다기 보다는 Timer를 매번 새로 생성을 해야 합니다. 두가지 경우를 샘플을 만들어 테스트 해보겠습니다

.

 

1. 타이머 생성 및 종료

 

먼저 타이머를 사용하기 위해 Activity에 선언을 합니다.

 

private Timer timerCall;
private int nCnt;

 

그리고 반복적으로 사용할 TimerTaskonCreate()에 생성합니다.

생성된 TimerTask Timer3초에 한번씩 호출 하도록 schedule 합니다.

 

TimerTask timerTask = new TimerTask() {
   
@Override
   
public void run() {
        someWork();
    }

);

nCnt = 0;

timerCall = new Timer();
timerCall.schedule(timerTask,0,3000);

 

3초에 한번씩 someWork()을 호출 하는 함수는 단순 로그 출력 입니다.

 

private void someWork() {

    Log.d("Test==>", nCnt + " work!!!");
   
if(nCnt >= 10) {
        timerCall.cancel();
    }


    nCnt++;
}

 

앱이 종료 할때까지 타이머가 작동하며 잘 출력 됩니다.

 

D/Test==>: 1 work!!!
D/Test==>: 2 work!!!
D/Test==>: 3 work!!!
D/Test==>: 4 work!!!
D/Test==>: 5 work!!!
D/Test==>: 6 work!!!
D/Test==>: 7 work!!!
D/Test==>: 8 work!!!
D/Test==>: 9 work!!!
D/Test==>: 10 work!!!

 

전체 소스

 

package copycoding.tistory.timertest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity {

   
private Timer timerCall;
   
private int nCnt;

   
@Override
   
protected void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);
        setContentView(R.layout.
activity_main);

       
nCnt = 0;

        TimerTask timerTask = new TimerTask() {
           
@Override
           
public void run() {
                someWork();
            }
        };

       
timerCall = new Timer();
       
timerCall.schedule(timerTask,0,1000);
    }

   
private void someWork() {

        Log.d(
"Test==>", nCnt + " work!!!");
       
if(nCnt >= 10) {
           
timerCall.cancel();
        }

       
nCnt++;
    }
}

 

 

2. 타이머 버튼 제어

 

이번에는 타이머를 버튼으로 제어를 해봅니다.   먼저 선언을 하고

 

private Timer timerCall;
private int nCnt;
TimerTask
timerTask;

 

onCreate()에 초기 값을 생성합니다.

 

nCnt = 0;

timerCall = new Timer();

 

start button을 클릭 하면 기존에 TimerTask가 있다면 삭제하고 다시 생성을 하여 타이머를 가동시켜주면 됩니다.

 

public void btnStart(View view) {
    Log.d(
"BTN Start==>", nCnt + " work!!!");
   
if(timerTask != null) {
       
timerTask.cancel();
    }
   
timerTask = new TimerTask() {
       
@Override
       
public void run() {
            someWork();
        }
    };
   
timerCall.schedule(timerTask,0,3000);
}

 

TimerTask를 삭제하지 않고 계속 생성 하면 기존 타이머가 죽지 않은 상황에서 계속 새로운 프로세스의 타이머가 만들어져 타이머로서의 기능을 할 수 없게 됩니다.

 

stop button을 클릭 하면 TimerTask가 작동중인지 확인하고 삭제를 하게 됩니다.

 

public void btnStop(View view) {
    Log.d(
"BTN Stop==>", nCnt + " work!!!");
   
if(timerTask != null) {
       
timerTask.cancel();
    }
}

 

타이머가 호출하는 작업 메소드는 단순히 증가하는 숫자를 로그로 남겨줍니다.

 

private void someWork() {

    Log.d(
"Test some work ==>", nCnt + " work!!!");

   
nCnt++;
}

 

화면은 그냥 버튼 2개만 추가 하였습니다.

 

 

로그에 남겨진 텍스트를 보면 어떻게 작동하는지 알 수 있습니다.  start 버튼에 cancel 로직을 주석처리해서 비교해 보면서 자신에 맞는 로직으로 수정하면 됩니다.

  

D/BTN Start==>: 0 work!!!
D/Test some work ==>: 0 work!!!
D/Test some work ==>: 1 work!!!
D/Test some work ==>: 2 work!!!
D/Test some work ==>: 3 work!!!
D/BTN Stop==>: 4 work!!!
D/BTN Start==>: 4 work!!!
D/Test some work ==>: 4 work!!!
D/Test some work ==>: 5 work!!!
D/Test some work ==>: 6 work!!!
D/BTN Stop==>: 7 work!!!

 

 

 

전체 소스

 

package copycoding.tistory.timertest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity {

   
private Timer timerCall;
   
private int nCnt;
    TimerTask
timerTask;

   
@Override
   
protected void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);
        setContentView(R.layout.
activity_main);

       
nCnt = 0;

       
timerCall = new Timer();

    }

   
private void someWork() {

        Log.d(
"Test some work ==>", nCnt + " work!!!");

       
nCnt++;
    }

   
public void btnStart(View view) {
        Log.d(
"BTN Start==>", nCnt + " work!!!");
       
if(timerTask != null) {
           
timerTask.cancel();
        }
       
timerTask = new TimerTask() {
           
@Override
           
public void run() {
                someWork();
            }
        };
       
timerCall.schedule(timerTask,0,3000);
    }

   
public void btnStop(View view) {
        Log.d(
"BTN Stop==>", nCnt + " work!!!");
       
if(timerTask != null) {
           
timerTask.cancel();
        }
    }
}

 

 

App을 새로 생성해서 테스트 하려고 하니 오류가 나옵니다.

 

Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager.

 

 

Gradle을 살펴보고

 

android {
   
compileSdkVersion
31
   
buildToolsVersion "31.0.0"

   
defaultConfig {
       
applicationId
"copycoding.tistory.sample"
       
minSdkVersion 16
       
targetSdkVersion 31
       
versionCode 1
       
versionName "1.0"

       
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   
}

 

설치된 SDK Tool도 살펴봐도

 

 

 

별 문제 없는것 같은데..

 

 

1. 버전 낮추기

 

다시 설치하긴 귀찮고 전체적으로 버전을 30으로 낮추어 봅니다. 

 

android {
   
compileSdkVersion
30
   
buildToolsVersion "30.0.3"

   
defaultConfig {
       
applicationId
"copycoding.tistory.sample"
       
minSdkVersion 16
       
targetSdkVersion 30
       
versionCode 1
       
versionName "1.0"

       
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'com.google.android.material:material:1.5.0'

 

 

이런.  새로운 오류가 발생 합니다.

 

 

ERROR:C:\Users\will\.gradle\caches\transforms-2\files-2.1\4eb2fc21ccc2dce4da190845bc3482ac\material-1.5.0\res\values-v31\values-v31.xml:3:5-94: AAPT: error: resource android:color/system_neutral1_1000 not found.

 

Material Component Library 버전이 업데이트 되어서 31을 사용해야 한다고 합니다.

답을 찾았네요.  material 버전을 낮추면 되는거군요.

 

android {
   
compileSdkVersion 30
   
buildToolsVersion "30.0.3"

    
defaultConfig {
       
applicationId
"copycoding.tistory.sample"
       
minSdkVersion 16
       
targetSdkVersion 30
       
versionCode 1
       
versionName "1.0"

       
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   
}

   
buildTypes {
       
release {
           
minifyEnabled
false
           
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       
}
    }
   
compileOptions {
       
sourceCompatibility JavaVersion.
VERSION_1_8
       
targetCompatibility JavaVersion.VERSION_1_8
   
}
}



dependencies {

   
implementation 'androidx.appcompat:appcompat:1.3.1'
   
implementation 'com.google.android.material:material:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
   
testImplementation 'junit:junit:4.+'
   
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

  

material 버전을 1.5.0에서 1.2.0으로 변경 하나 잘 됩니다.  물론 1.4.0로 낮추어도 됩니다.

 

 

 

 

2. buildToolsVersion 낮추기

 

다른건 그대로 두고 buildToolsVersion만 낮추어서 해결해 봅니다.

Android Studio에서 Gradle을 열고 오른쪽 상단을 보면 Open (Ctrl+Alt+Shift+S)가 있는데

 

 

이걸 누르면 창이 하나 뜨고

 

 

여기서 Build Tools Version을 확장하여 31.0.0 대신 하나 낮은걸 선택 합니다.

  

android {
   
compileSdkVersion
31
   
buildToolsVersion '30.0.3'

   
defaultConfig {
       
applicationId
"copycoding.tistory.test"
       
minSdkVersion 16
       
targetSdkVersion 31
       
versionCode 1
        
versionName "1.0"

 

 

 

그리고 실행하면 다른 오류가 발생 합니다.

 

Manifest merger failed : Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported` when the corresponding component has an intent filter defined. See https://developer.android.com/guide/topics/manifest/activity-element#exported for details.

 

 

android:exported 이게 문제라는 군요.  참조하라는 링크를 따라가보면 설명이 있습니다.

https://developer.android.com/guide/topics/manifest/activity-element#exported

 

어째든 android:exportedManifest에 추가하고 값을 넣으면 됩니다.  만일 false를 넣으면 앱이 생성은 되는데 실행하려고 하면 app is’nt installed라는 토스트만 나오니 true를 적어 줍니다.

값을 설정하는 위치는 activity에 해주어야 합니다.

  

<activity android:name=".MainActivity"
   
android:exported="true"
>
    <
intent-filter>
        <
action android:name="android.intent.action.MAIN" />

        <
category android:name="android.intent.category.LAUNCHER" />
    </
intent-filter>
</
activity>

 

이렇게 하면 앱을 잘 테스트할 수 있습니다.

 

 

이건 제가 간단한 테스트를 하려고 한것이고 실제로는 높은 버전으로 컴파일 해야 최신 핸드폰에서도 잘 돌아가는 앱을 만들 수 있겠죠.

 

- copy coding -


12345···15

+ Recent posts