2019년 10월 23일 수요일

안드로이드 화면 전환하기






1. Layout Inflation (레이아웃 인플레이션)

앱이 실행될 때 XML 레이아웃의 내용이 메모리에 객체화되고 객체화된 XML 레이아웃을 소스 파일에서 사용한다.
XML 레이아웃의 내용이 메모리에 객체화 되는 과정을 Inflation(인플레이션)이라고 한다.



* setContentView() : 레이아웃을 메모리에 객체화 하는 메소드

- 인자로 R.layout.activity_main 과 같이 받아서, 해당 레이아웃 XML 파일을 메모리에 객체화한다.
- 화면 전체(메인 화면)에 나타낼 뷰를 지정할 수 있다.
- 화면의 일부분을 차지하는 부분 레이아웃을 객체화할 수는 없다.
- 부분 레이아웃을 메모리에 객체화 하려면 인플레이터를 사용해야 한다.





2. LayoutInflater (부분 화면) 클래스 사용하기

안드로이드는 부분 화면을 객체화 하여 화면에 띄우기 위해 시스템 서비스로 LayoutInflater 라는 클래스를 제공한다.
getSystemService() 메서드를 사용하여 LayoutInflater 객체를 참조할 수 있다.
cf) 시스템 서비스는 단말이 시작되면서 항상 실행되는 서비스이다.
혹은 LayoutInflater.from() 를 이용하여 LayoutInflater 객체를 참조할 수도 있다.



cf) 프로젝트에서 액티비티 추가하기
[app 우클릭] -> [New] -> [Activity] -> [Empty Activity] 



Activity Name을 MenuActivity 라고 입력하고 Finish 클릭


MenuActivity로 실습을 할것이므로 Manifest 파일에 첫 실행 액티비티를 위처럼 수정한다.






public class MenuActivity extends AppCompatActivity {
    
    LinearLayout container;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_menu);
        
        container = findViewById(R.id.container);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            /**
             * 버튼을 클릭했을 때 부분화면을 전체 레이아웃의 리니어 레이아웃에 추가한다.
             * @param v
             */
            @Override
            public void onClick(View v){
                LayoutInflater inflater = (LayoutInflater) getSystemService(
                        Context.LAYOUT_INFLATER_SERVICE);
                // 부분 레이아웃(sub1.xml)을 메모리에 객체화
                // container에 부분 레이아웃을 붙인다.
                inflater.inflate(R.layout.sub1, container, true);
                // 그 후 container에서 부분 레이아웃의 요소를 참조할 수 있게 된다.
                CheckBox checkBox = container.findViewById(R.id.checkBox);
                checkBox.setText("로딩되었어요.");
            }
        });
    }
}





sub1.xml




activity_menu.xml




추가하기 버튼을 클릭하면 부분화면이 아래에 추가된다.





3. 여러 화면 만들고 화면간 전환하기


프로젝트에 MenuActivity를 새로 추가하면 Manifest 파일에 자동으로 추가된다.
label 속성은 화면의 제목, theme 는 대화상자 형태로 액티비티를 설정한다.



activity_menu.xml
단순하게 버튼 한개만 추가한 형태이다.
이 화면은 대화상자 형태로 표시 될 것이다.


activity_main.xml
메인 액티비티 역시 버튼 하나로 구성되었다.

메인 액티비티에서 버튼을 클릭하면, 메뉴 액티비티를 띄울 것이고
메뉴 액티비티에는 돌아가기 버튼이 있어 클릭 시 메인 액티비티로 돌아가게 된다.




MainActivity를 작성하기 앞서 부모 클래스들에게서
상속받은 메서드들 중 하나인 onActivityResult()를 Override 하여 구현한다.
새로 띄운 액티비티로부터 응답을 처리 하기 위한 메소드 이다. 


MainActivity.java


public class MainActivity extends AppCompatActivity {
    // 새 액티비티를 띄울 때 보낼 코드
    public static final int REQUEST_CODE_MENU = 101;

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

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            /**
             * 버튼을 클릭했을 때 새로운 액티비티(MenuActivity)를 띄운다.
             * @param v
             */
            @Override
            public void onClick(View v) {
                // 액티비티를 띄우기 위해 인텐트 객체를 사용한다.
                // 또한 액티비티간에 데이터를 전달할 수 있다.
                // 이벤트 처리 메서드 안에서 this 변수로 MainActivity 객체를 참조할 수 없으므로
                // getApplicationContext() 를 사용하였다.
                // Intent 생성자 인자로 MenuActivity.class를 넣음으로써 이와 연결된다.
                Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
                // startActivityForResult 는 새 액티비티로부터 응답을 받을 수 있다.
                startActivityForResult(intent, REQUEST_CODE_MENU);
            }
        });
    }

    /**
     * 새로 띄었던 액티비티가 응답을 보내오면 그 응답을 처리한다.
     * @param requestCode : 액티비티를 띄울 때 전달했던 요청 코드
     * @param resultCode : 새 액티비티로부터 전달된 응답코드 (새 액티비티에서 처리한 결과가 정상인지 여부 판단)
     * @param data : 새 액티비티로부터 받은 인텐트
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if(requestCode == REQUEST_CODE_MENU) {
            Toast.makeText(getApplicationContext(),
                    "onActivityResult 호출됨. 요청 코드 : " + requestCode +
                            ", 결과 코드 : " + resultCode, Toast.LENGTH_LONG ).show();
            if(resultCode == RESULT_OK){
                //인텐트로부터 데이터 꺼내오기 
                String name = data.getStringExtra("name");
                Toast.makeText(getApplicationContext(), "응답으로 전달된 name : " + name, Toast.LENGTH_LONG).show();
            }
        }
    }
}


* 새로 띄울 액티비티(MenuActivity.java)


public class MenuActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_menu);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                // 인텐트 객체 생성
                Intent intent = new Intent();
                // name 값을 부가 데이터로 넣기
                intent.putExtra("name", "mike~");
                // 새로 띄운 액티비티에서 이전 액티비티로 인텐트를 전달하기
                setResult(RESULT_OK, intent);
                // 현재 액티비티 화면에서 없애기
                finish();
            }
        });
    }
}


버튼을 클릭하면 다이얼로그 방식의 MenuActivity가 보여진다.


메인 액티비티로 돌아가게 되면 응답을 처리하는 메서드가 호출되며
요청, 결과 코드를 확인할 수 있다.
Toast가 두개여서 나중것이 가려졌는데 Toast 한개를 주석처리하면 확인 할 수 있다.





4. Intent (인텐트) 사용하기

인텐트는 앱 구성 요소 간에 작업 수행을 위한 정보를 전달하는 역할을 한다.


* 인텐트의 기본 구성 요소 : 액션(Action) , 데이터(Data)

액션은 수행할 기능이며 데이터는 수행될 대상의 데이터를 의미한다.

액션과 데이터를 이용해 인텐트 객체를 생성하고 필요한 액티비티를 띄어줄 수 있다.

* 인텐트 종류 : 명시적 인텐트, 암시적 인텐트
명시적 인텐트는 클래스 객체나 컴포넌트 이름을 지정하여 호출할 대상을 확실히 아는 경우를 말한다.
암시적 인텐트는 액션과 데이터를 지정하였으나 호출할 대상이 달라질 수 있는 경우를 말한다.

- 암시적 인텐트의 여러가지 속성


  • 범주(Category) : 액션이 실행되는데 필요한 추가적 정보
  • 타입(Type) : 인텐트에 들어가는 데이터의 MIME 타입을 명시적으로 지정
  • 컴포넌트(Component) : 인텐트에 사용될 컴포넌트 클래스 명을 명시
  • 부가 데이터(Extra Data) : 추가적인 정보가 들어있는 번들 데이터를 전달


아래 코드는 인텐트에 액션과 데이터를 넣어 다른 앱의 액티비티를 띄우는 코드이다.


단순히 입력상자와 버튼을 만들었다.
입력상자에는 전화번호 포맷 형식의 데이터를 입력한다.



public class MainActivity extends AppCompatActivity {

    EditText editText;

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

        editText = findViewById(R.id.editText);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //입력 상자에 입력된 전화번호 가져오기
                String data = editText.getText().toString();
                //전화 걸기 화면을 보여줄 인텐트 객체 생성
                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(data));
                //액티비티 띄우기 (액티비티 메모리에 올리기)
                startActivity(intent);
            }
        });
    }
}


전화걸기 버튼을 클릭했을 때,
자동으로 전화번호 앱이 실행되는 것을 확인할 수 있다.


아래 두번째 코드는 컴포넌트 이름을 이용해 새로운 액티비티를 띄우는 경우이다.
위 화면 레이아웃에서 id가 button2인 버튼을 하나더 추가하고, 새로운 액티비티(MenuActivity)를 하나 생성한다.


Button button2 = findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                //컴포넌트 이름을 지정할 수 있는 객체 생성
                //ComponentName 생성자의 인자로 순서대로 패키지명과, 클래스 명이 된다.
                ComponentName name = new ComponentName("org.techtown.samplecallintent",
                        "org.techtown.samplecallintent.MenuActivity");
                // 인텐트 객체에 컴포넌트를 지정한다.
                intent.setComponent(name);
                startActivityForResult(intent,101);
            }
        });


컴포넌트이름을 지정할 때 클래스 명은 앞에 패키지 경로까지 전부다 적어주어야 한다.
인텐트를 이용해 다른 화면을 띄울 수가 있는데 직접 만든 화면이나, 다른 사람이 만든 앱의 화면을 띄울 수도 있다.





5. 플래그와 부가 데이터 사용하기

startActivity 또는 startActivityForResult 메서드를 여러 번 호출 하게 될 경우 메모리에 여러 개가 중복되어 만들어질 것이다.
따라서 중복된 액티비티는 띄우지 않게 하기 위해 플래그를 사용한다.

액티비티가 만들어질 때마다 액티비티 스택이라는 곳에 차곡차곡 쌓이게 되는데, startActivity 또는 startActivityForResult를 호출하게 되면 이전의 액티비티는 액티비티 스택에 저장되고 새로 만들어진 액티비티가 화면에 보이게 된다.
 즉 가장 상위에 쌓인 것부터 화면에 보여지면서 back 시스템 버튼을 눌렀을 때 그 아래의 액티비티가 보여지게 된다.

동일한 액티비티들을 여러 번 실행한다면 동일한 액티비티가 스택에 여러 개 들어가게 되는 문제를 해결해주는 것이 플래그이다.

* 대표적인 플래그들
  • FLAG_ACTIVITY_SINGLE_TOP : 이미 생성된 액티비티가 있다면 그 액티비티를 그대로 사용하라는 플래그. 기존에 사용하는 액티비티에서 인텐트 객체만 전달받으려면 onNewIntent()를 오버라이드하면 된다.
  • FLAG_ACTIVITY_NO_HISTORY : 처음 이후에 실행된 액티비티는 액티비티 스택에 쌓이지 않는다.
  • FLAG_ACTIVITY_CLEAR_TOP : 이것이 설정되어 있는 액티비티 위에 있는 다른 액티비티를 모두 종료하게 된다. 홈 화면과 같이 항상 우선하는 액티비티를 만들때 유용함.



* 인텐트에 객체를 직렬화하여 데이터 보내기

객체를 직렬화 하기 위해서는 데이터를 담고 있는 클래스는 Parcelable 인터페이스를 구현해야한다.
자바의 Serializable 직렬화와 동일한 개념이다.

* Parcelable 직렬화 필수 구현
  1. CREATOR 상수 정의
  2. describeContents() 구현
  3. writeToParcel(Parcel dest, int flags) 구현



import android.os.Parcel;
import android.os.Parcelable;

/*
    안드로이드에서의 직렬화는 Parcelable 인터페이스를 구현함으로써 사용할 수 있다.
 */
public class SimpleData implements Parcelable {

    int number;
    String message;

    public SimpleData(int num, String msg){
        number = num;
        message = msg;
    }

    /**
     * SimpleData의 생성자의 인자로 Parcel 객체를 받아들이는데
     * Parcel 객체에서 데이터를 읽어들이는 역할을 한다.
     * @param src
     */
    public SimpleData(Parcel src){
        number = src.readInt();
        message = src.readString();
    }

    /**
     * CREATOR 상수를 정의해야한다.
     * Parcel 객체를 읽어들이고, 객체를 생성하는 역할
     * 반드시 static final 로 선언해야 한다.
     */
    public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {

        public SimpleData createFromParcel(Parcel in){
            return new SimpleData(in);
        }

        public SimpleData[] newArray(int size){
            return new SimpleData[size];
        }

    };

    /**
     * 필수 구현
     * @return
     */
    public int describeContents(){
        return 0;
    }

    /**
     * 필수 구현
     * SimpleData 객체에 있는 데이터를 Parcel 객체로 만드는 역할을 한다.
     * @param dest
     * @param flags
     */
    public void writeToParcel(Parcel dest, int flags){
        dest.writeInt(number);
        dest.writeString(message);
    }

}



public class MainActivity extends AppCompatActivity {

    public static final int REQUEST_CODE_MENU = 101;
    public static final String KEY_SIMPLE_DATA = "data";

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

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
                // 객체를 생성
                SimpleData data = new SimpleData(100, "Hello Android!!");
                // 인텐트에 객체를 보낸다.
                intent.putExtra(KEY_SIMPLE_DATA, data);
                startActivityForResult(intent, REQUEST_CODE_MENU);
            }
        });
    }
}



public class MenuActivity extends AppCompatActivity {

    TextView textView;

    public static final String KEY_SIMPLE_DATA = "data";

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

        textView = findViewById(R.id.textView);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.putExtra("name", "mike");
                setResult(RESULT_OK, intent);

                finish();

            }
        });
        /**
         * 메인 액티비티가 전달한 인텐트를 참조하기 위해 onCreate()안에서 getIntent()를 호출
         */
        Intent intent = getIntent();
        processIntent(intent);

    }

    public void processIntent(Intent intent){
        if(intent != null){
            //Bundle 객체를 반환한다.
            Bundle bundle = intent.getExtras();
            //Bundle 객체에서 키를 이용하여
            //getParcelable메서드를 호출하면 SimpleData 객체을 얻어낼 수 있다.
            SimpleData data = bundle.getParcelable(KEY_SIMPLE_DATA);

            textView.setText("전달 받은 데이터\nNumber: "+data.number +
                    "\nMessage:"+data.message);

        }
    }
}





6. 태스크(Task) 관리 이해하기

태스크는 앱이 어떻게 동작할지 결정하는데 사용된다.
태스크를 이용하면 프로세스처럼 독립적인 실행 단위와 상관없이 어떤 화면들이 같이 동작해야 하는지 흐름을 관리할 수 있다.

프로세스끼리는 정보를 공유할 수가 없다.
따라서 하나의 프로세스에서 다른 프로세스의 화면을 띄우려면 시스템의 도움이 필요하다. 
시스템에서 이런 액티비티의 각종 정보를 저장해두기 위해 태스크라는 것을 만든다.

시스템에서는 액티비티들의 정보를 저장해두기 위해 태스트라는 것을 만들고 알아서 관리하지만 직접 제어할 수도 있다.

AndroidManifest.xml 에 액티비티를 등록할 때 태스크도 함께 설정할 수 있다.


public class MainActivity extends AppCompatActivity {

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

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                //나 자신을 띄운다.
                Intent intent = new Intent(getApplicationContext(), MainActivity.class);
                startActivity(intent);

            }
        });
    }
}

위 코드는 버튼을 클릭할 때마다 자기자신의 액티비티를 스택에 넣는다. 이것은 AndroidManifest.xml 파일에서 activity 태그의 속성에서 android:launchMode="standard" 을 넣어주는 것과 같다.

만약 android:launchMode="singleTop" 으로 설정한다면 태스크의 가장 위쪽에 있는 액티비티는 더 이상 새로이 만들지 않게된다.
이는 5번의 플래그의 FLAG_ACTIVITY_SINGLE_TOP 와 같은 효과이다.
따라서 중복되어 액티비티가 생성되지 않으므로 (onCreate메서드가 실행되지 않음)
인텐트는 onNewIntent()메서드로 전달받으면 된다.

singleTask : 액티비티가 실행되는 시점에 새로운 태스크를 만들게 된다.
singleInstance: 액티비티가 실행되는 시점에 새로운 태스크를 만들면서, 그 이후에 실행되는 액티비티들은 이 태스크를 공유하지 않는다.





7. 액티비티의 수명주기와 SharedPreferences 이해하기


onCreate -> onStart -> onResume -> onPause ->onStop -> onDestroy


앱이 갑자기 중지되어 앱 데이터의 저장과 복원이 필요할 때가 있다.
간단한 데이터를 저장하거나 복원시에는 SharedPreferences 를 사용할 수 있다.


아래 코드는 앱을 실행했을 때 입력상자에 사람 이름을 입력한 상태에서, 앱을 종료한 후 다시 실행했을 때 사람 이름이 그대로 보이도록 만드는 내용이다.

앱이 갑자기 중지 될때 항상 호출되는 메서드는 onPause이다.
앱이 시작될 때 항상 호출되는 메서드는 onResume이다.
즉 중지 시에 데이터를 저장하고 시작할 때 데이터를 복원하면 된다. 




public class MainActivity extends AppCompatActivity {

    EditText nameInput;

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

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
                startActivity(intent);
            }
        });
        Toast.makeText(this, "onCreate 호출됨.",Toast.LENGTH_LONG).show();
        nameInput = findViewById(R.id.nameInput);
        
    }

    @Override
    protected void onStart() {
        super.onStart();
        println("onStart 호출됨.");
    }

    @Override
    protected void onStop() {
        super.onStop();
        println("onStop 호출됨.");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        println("onDestroy 호출됨.");
    }

    public void println(String data){
        Toast.makeText(this, data, Toast.LENGTH_LONG).show();
        //Logcat에 로그 출력하기
        Log.d("Main", data);
    }

    @Override
    protected void onPause() {
        super.onPause();
        Toast.makeText(this, "onPause 호출됨", Toast.LENGTH_LONG).show();
        //현재 입력상자에 입력된 데이터를 저장한다.
        saveState();
    }

    @Override
    protected void onResume() {
        super.onResume();
        Toast.makeText(this, "onPause 호출됨", Toast.LENGTH_LONG).show();
        //설정 정보에 저장된 데이터를 복원한다.
        restoreState();
    }

    protected  void restoreState(){
        SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        if( (pref != null) && (pref.contains("name"))){
            String name = pref.getString("name","");
            nameInput.setText(name);
        }
    }

    protected void saveState(){
        SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putString("name", nameInput.getText().toString());
        editor.commit(); //커밋을 해야 데이터가 저장이 된다.
    }

    protected void clearState(){
        SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.clear();
        editor.commit();
    }
}













2019년 10월 22일 화요일

안드로이드 이벤트 처리





1. 터치 이벤트, 제스처 이벤트

터치 이벤트는 화면을 손가락으로 누를 때 발생하는 이벤트들을 처리할 수 있다.
제스처 이벤트는 터치 이벤트 중에서 일정한 패턴, 스크롤과 같은 이벤트를 처리할 수가 있다.
제스처 이벤트는 터치 이벤트를 받은 후에 추가적인 확인을 거쳐 만들어진다.




package org.techtown.sampleevent;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    TextView textView;
    GestureDetector detector;

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

        textView = findViewById(R.id.textView);

        View view = findViewById(R.id.view);
        /**
         * 뷰에 OnTouchListener 를 등록한다.
         */
        view.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent){
                // 액션의 상태 값을 가져오는 메소드
                int action = motionEvent.getAction();
                // 현재 X, Y 좌표의 위치를 가져옴
                float curX = motionEvent.getX();
                float curY = motionEvent.getY();

                if(action == MotionEvent.ACTION_DOWN) {
                    println("손가락 눌림 : "+ curX +" , " + curY);
                }else if( action == MotionEvent.ACTION_MOVE) {
                    println("손가락 움직임 : " + curX + " , " +  curY);
                }else if(action == MotionEvent.ACTION_UP) {
                    println("손가락 뗌 : " + curX + " , " + curY);
                }
                return true;
            }
        });

        //제스쳐 이벤트를 생성
        detector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                println("onDown() 호출됨.");
                return true;
            }

            @Override
            public void onShowPress(MotionEvent e) {
                println("onShowPress() 호출됨.");
            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                println("onSingleTapUp() 호출됨.");
                return true;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                println("onScroll() 호출됨 : "+ distanceX + " , " + distanceY);
                return true;
            }

            @Override
            public void onLongPress(MotionEvent e) {
                println("onLongPress() 호출됨.");
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                println("onFling() 호출됨 : "+ velocityX + ", " + velocityY);
                return true;
            }
        });
        /**
         * 두번째 뷰에다 제스쳐이벤트를 설정
         */
        View view2 = findViewById(R.id.view2);
        view2.setOnTouchListener(new View.OnTouchListener(){
                @Override
                public boolean onTouch(View view, MotionEvent motionEvent ){
                    // 뷰를 터치했을때 발생하는 터치이벤트를 제스터 디렉터로 전달한다.
                    detector.onTouchEvent(motionEvent);
                    return true;
                }
        });
    }

    public void println(String data) {
        textView.append(data + "\n");
    }

}



스크롤뷰 -> 리니어레이아웃 -> 텍스트뷰에서 이벤트 정보를 찍어보기로 한다.


파란 부분이 첫번째 뷰, 주황 부분이 두번째 뷰이다.
이벤트가 발생할때마다 아래의 스크롤뷰에 수직으로 데이터들이 쌓인다.





2. 키 이벤트 처리하기

키 입력은 시스템 버튼이 눌렸거나, 문자열 키가 입력된 경우에 관하여 이벤트 처리를 할 수 있다.


- 시스템 [back] 버튼 눌렸을 때 토스트 메시지 생성하기
시스템 버튼인 back은 onKeyDown 메서드를 재정의 하여 간단히 이벤트 처리할 수 있다.


MainActivity.java 에서 우클릭 한 후 [Generate] 선택
[Override Methods] 선택


[onKeyDown] 메소드를 선택하고 [OK] 하면 메소드가 자동으로 생성된다.





@Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        
        if(keyCode == KeyEvent.KEYCODE_BACK){
            Toast.makeText(this, "시스템 [back] 버튼이 눌렸습니다.",
                    Toast.LENGTH_LONG).show();
            return true;
        }
        return false;
    }





3. 단말 방향 전환 시 이벤트 처리

단말의 방향 전환 시에는 앱화면에 대하여 가로, 세로 각각 XML 레이아웃을 만들 필요가 있다.
단말의 방향이 바뀌었을 때는 액티비티는 메모리에서 없어졌다가 다시 만들어지게 된다.


/app/res/layout/ : 단말 세로 방향
/app/res/layout-land/ : 단말 가로 방향


[res] 폴더에서 우클릭하여 [New] -> [Android Resource Directory] 클릭


Directory name 을 layout-land 로 입력하고 OK.


Project Files 구조로 볼때만 layout-land 폴더가 보이게 된다.
생성된 것을 확인.


layout 폴더와 layout-land 는 같은 역할을 하지만 단말이 가로방향으로 보일 때는 layout-land 폴더 안에 있는 XML 레이아웃 파일이 사용된다.
layout-land 폴더가 없다면 어떤 방향이든 layout 폴더의 XML 파일을 디폴트로 사용하게 된다.


단말의 방향이 바뀌었을 때는 액티비티는 메모리에서 없어졌다가 다시 만들어지게 된다면, 이전에 선언해 두었던 변수 값이 사라지므로 이를 유지하도록 해야한다.

이를 위해 onSaveInstanceState 콜백 메서드가 제공된다.
이 메서드는 액티비티가 종료되기 전의 상태를 저장한다.
이때 저장한 상태는 onCreate() 메서드가 호출될 때 전달되는 번들 객체로 복원할 수 있다.




public class MainActivity extends AppCompatActivity {
    String name;

    EditText editText;

    /**
     * onCreate의 파라미터를 보면 Bundle 객체인 savedInstanceState를 인자로 받게 되어 있다.
     * 이 객체에서 데이터를 가져와서 name 변수에 다시 할당하게 되어 복구되는 것이다.
     * @param savedInstanceState
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        showToast("onCreate 호출됨.");

        editText = findViewById(R.id.editText);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view){
                // 버튼을 클릭했을 때 사용자가 입력한 값을 name 변수에 저장
                name = editText.getText().toString();
                showToast("입력된 값을 변수에 저장하였습니다 : "+ name);
            }
        });
        // 이 화면이 초기화 될 때 name 변수값 복원
        // ex) 단말 방향 전환시 액티비티가 메모리에서 제거되는 상황
        if(savedInstanceState != null){
            name = savedInstanceState.getString("name");
            showToast("값을 복원하였습니다 : "+name);
        }

    }
    /**
     * onSaveInstanceState 메소드는
     * 액티비티가 종료되기 전의 상태를 저장한다.
     * @param outState
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString("name", name);
    }

    @Override
    protected void onStart() {
        super.onStart();
        showToast("onStart 호출됨.");
    }

    @Override
    protected void onStop() {
        super.onStop();
        showToast("onStop 호출됨.");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        showToast("onDestroy 호출됨.");
    }

    public void showToast(String data){
        Toast.makeText(this, data, Toast.LENGTH_LONG).show();
    }

}





4. 단말 방향이 바뀔 때 액티비티 유지하는 방법

- /app/manifests/AndroidManifest.xml 수정

아래와 같이 빨간네모상자의 속성을 설정하면, 시스템은 단말의 방향이 바뀌는 시점에 액티비티에게 상태 변화를 알려줄 수 있게 된다. 
keyboardHidden 값은 키패드가 자동으로 나타나지 않도록 하는 설정이다.


activity 태그가 액티비티를 등록할 때 사용하는 태그이다.
configChanges 속성을 위와 같이 추가한다.





public class MainActivity extends AppCompatActivity {

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

    /**
     * 액티비티에서 방향 전환을 인식할 수 있게 되어,
     * 단말의 방향이 바뀌는 시점에 자동으로 이 메서드가 호출된다.
     * @param newConfig
     */
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        if(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            showToast("가로 방향: ORIENTATION_LANDSCAPE");
        }else if(newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
            showToast("세로 방향: ORIENTATION_PORTRAIT");
        }
    }
    public void showToast(String data){
        Toast.makeText(this, data, Toast.LENGTH_LONG).show();
    }

}




5. 단말 방향을 고정시키기

- /app/manifests/AndroidManifest.xml 수정


screenOrientation 속성에서 landscape로 설정하면 화면이 가로로 고정된다.




2019년 10월 21일 월요일

안드로이드 위젯 기초





1. String 문자열 파일 xml로 관리하기

다국어 지원을 하거나, 문자열의 관리를 쉽게 한다.



/app/res/values/string.xml 에 문자열을 정의함.




@string/... 과 같은 형식으로 참조한다.


cf) 다국어 문자열 파일 관리 방법

Android 폴더 보기 형식에서는 아래와 같은 구조를 확인할 수 없으므로 상단에서 Project Files로 변경하면 아래와 같은 구성이 보인다.


values-en, values-ko 처럼 values 뒤에 로케일을 붙여서 폴더를 생성하고
string.xml파일을 각 나라의 언어로 정의하면 된다. 기본 폴더인 values는 삭제하지 않는다.
단말에 설정된 언어에 해당하는 파일이 없는 경우 기본 폴더를 참조하기 때문이다.



2. 텍스트 뷰의 속성

-textColor : 텍스트 색상 설정


#AARRGGBB 의 형태
Alpha: 투명도, FF(투명하지 않음), 88(중간), 00(투명함)
Red: 빨강
Green: 초록
Blue: 파랑

-textStyle : 텍스트 스타일 설정
normal|bold|italic 와 같이 | 파이프라인을 이용하여 여러 속성을 함께 지정할 수 있다.

-typeFace : 문자열의 폰트 설정

-maxLines : 텍스트 뷰에서 표시하는 만주열의 최대 줄 수를 설정


maxLines을 1로 지정하고 한줄을 초과하는 text를 작성해본다.


maxLines이 1이므로 한 줄로 표시된다.

-selectAllOnFocus : true/false 로 포커스를 받을 때 문자열 전체가 선택 여부
-cursorVisible : true/false 로 커서 보임 여부
-autoLink : true/false 로 웹페이지 주소나, 이메일 주소를 링크 색상으로 표시
-lineSpacingMultiplier : 줄 간격을 기본 줄 간격의 배수로 설정
-lineSpacingExtra : 여유 값으로 설정할 때 사용
-capitalize : 글자, 단어, 문장 단위로 대소문자 조절
-ellipsize : 줄임 표시 관련 속성
-editable : 편집 가능 관련 속성



3. 버튼 속성

버튼은 텍스트뷰를 상속하여 정의되어 있으므로 텍스트뷰의 속성을 그대로 사용할 수 있다.









4. 에디트 텍스트 속성

-hint : 기본 안내문의 hint 표시
-inputType : 입력되는 글자의 유형을 정의할 수 있다. 글자를 입력할 때 보이는 키패드의 유형을 정의할 수 있다.



에디트텍스트 속성 추가하기
커서가 깜박이며 아래 글자키패드가 보여진다.





5. 이미지를 해상도에 따라 다르게 보여주기
/app/res 하위에 drawable 디렉토리를 아래와 같이 추가로 생성해주고 알맞은 이미지를 넣는다.


/app/res/drawable : 일반적으로 사용하는 기본 이미지
/app/res/drawable-xhdpi : 초고해상도
/app/res/drawable-hdpi : 고해상도
/app/res/drawable-mdpi : 중간해상도
/app/res/drawable-ldpi : 저해상도





6. 드로어블 만들기


드로어블은 뷰에 설정할 수 있는 객체이며, 그 위에 그래픽을 그릴 수 있다.
드로어블은 XML 파일이나 소스 파일로 작성할 수 있다.
drawable 폴더 안에 이미지가 아닌 XML 파일이 들어가서 이미지처럼 설정된다.



* 상태 드로어블 만들기
res/drawable 폴더를 우클릭 한 후 [Drawable resource file] 을 클릭
file name을 지정하고 OK.


selector 안에 item 요소를 추가한다.
버튼이 눌린 이미지에는 state_pressed를 true로 설정하여
버튼이 눌렸을때 눌린 이미지가 나오도록 설정한다.
그 다음줄에 아무런 상태가 설정되어 있지 않은 item은
버튼에서 손을 땐 기본 상태이며 finger 이미지가 나오게 된다.


그리고 버튼의 background 속성에 @drawable/xml파일명 을 적어서 연결하면
클릭했을때 이미지가 변하는 버튼으로 설정된다.




* 셰이프 드로어블 만들기





shape 로 바꾸고 모양을 사각형으로 설정하고 디자인 요소를 설정한다.
이것을 버튼의 background 에 @drawable/xml파일명으로 설정해주면 된다.


* 배경 그라데이션 드로어블 만들기


그라데이션을 주려면 gradient 속성을 이용하면 된다.



* 드로어블을 이용하여 테두리만 있는 버튼 만들기




layer-list 안에 item 태그를 여러개를 넣을 수 있으며
 그래픽을 중첩시켜서 표현 할 수도 있다.





위에서 작성한 드로어블들이 반영된 모습





7. 토스트, 스낵바, 대화상자 만들기


- 토스트 메시지 위치 변경 시키기


public class MainActivity extends AppCompatActivity {

    EditText editText;
    EditText editText2;

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

        editText = findViewById(R.id.editText);
        editText2 = findViewById(R.id.editText2);

    }

    public void onButton1Clicked(View v){
        try{
            Toast toastView = Toast.makeText(this, "위치가 바뀐 토스트 메시지 입니다.",
                    Toast.LENGTH_LONG);

            //위치값을 에디트텍스트에서 값을 받아와서,
            int xOffset = Integer.parseInt(editText.getText().toString());
            int yOffset = Integer.parseInt(editText2.getText().toString());

            //setGravity() 를 이용하여 토스트 위치를 바꿔준다.
            toastView.setGravity(Gravity.TOP|Gravity.TOP, xOffset, yOffset);
            toastView.show();

        }catch (NumberFormatException e){
            e.printStackTrace();
        }
    }

}


토스트 메시지의 위치가 입력한 위치에 맞춰 나타난다.



-토스트 메시지 모양 바꿔 보여주기


public void onButton2Clicked(View v){
        //LayoutInflater 객체를 이용해 XML 로 정의된 레이아웃을 메모리에 객체화 함
        LayoutInflater inflater = getLayoutInflater();

        //layout 폴더의 toastborder.xml 파일의 toast_layout_root 아이디를 가져옴
        View layout = inflater.inflate(R.layout.toastborder,
                (ViewGroup) findViewById(R.id.toast_layout_root));

        //View 객체에서 아이디가 text 인 텍스트뷰 객체를 가져옴
        TextView text = layout.findViewById(R.id.text);

        Toast toast = new Toast(this);
        text.setText("모양 바꾼 토스트");
        toast.setGravity(Gravity.CENTER, 0, -100);
        toast.setDuration(Toast.LENGTH_SHORT);
        toast.setView(layout);
        toast.show();

}



/app/layout/toastborder.xml


/app/drawable/toast.xml









- Snackbar(스낵바) 보여주기
간단한 메시지를 보여줄 때 토스트 대신 스낵바를 사용하는 경우도 많다.
스낵바는 외부 라이브러리로 추가 되었기 때문에 스낵바가 들어있는 디자인 라이브러리를 프로젝트에 추가해야 사용할 수 있다.

* 외부 라이브러리 추가하기


[File] -> [Project Structure] 클릭


좌측 Dependency 메뉴를 클릭 한 후 app을 선택. (안드로이드 스튜디오 3.5.1버전 화면)
우측에 + 버튼을 클릭하여 Library Dependency 를 선택한다.


스낵바에서 사용하는 외부라이브러리를 검색 하고 버전을 선택 후 OK 클릭


그러면 라이브러리가 추가된 것이 보이고 OK 하면
프로젝트가 재빌드 되면서 해당 라이브러리를 사용할 수 있게 된다. 



public void onButton3Clicked(View v){
       Snackbar.make(v, "스낵바입니다..~", Snackbar.LENGTH_LONG).show();
}


버튼을 클릭했을 때 하단에 스낵바 생성됨


- 알림 대화 상자(Dialog) 보여주기



public class MainActivity extends AppCompatActivity {

    TextView textView;

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

        textView = findViewById(R.id.textView);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                showMessage();
            }
        });
    }
    private void showMessage(){
        //다이얼로그 빌더를 이용하여 다이얼로그 객체를 생성
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        //알림창 제목
        builder.setTitle("안내");
        //알림창 내용
        builder.setMessage("종료하시겠습니까?");
        //아이콘 설정
        builder.setIcon(android.R.drawable.ic_dialog_alert);

        builder.setPositiveButton("예", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                String message = "예 버튼이 눌렸습니다";
                textView.setText(message);
            }
        });

        builder.setNeutralButton("취소", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                String message = "취소 버튼이 눌렸습니다.";
                textView.setText(message);
            }
        });

        builder.setNegativeButton("아니오", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                String message = "아니오 버튼이 눌렸습니다";
                textView.setText(message);
            }
        });

        //대화 상자 객체를 만든다.
        AlertDialog dialog = builder.create();
        //대화 상자를 화면에 보여준다.
        dialog.show();

    }
}



버튼을 클릭했을때 알림창이 뜬다.





8. 프로그레스바 (상태 진행바) 만들기

어떤 일의 진행 상태를 보여주기 위해 프로그레스바를 만든다.



public class MainActivity extends AppCompatActivity {
    ProgressDialog dialog;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        //프로그레스바에 현재 상태값 설정하기
        ProgressBar progressBar = findViewById(R.id.progressBar);
        //setIndeterminate을 true 로 하게 되면 프로그레스바 진행 움직임 애니메이션이 무한정 나오게 된다. 
        // 이를 false로 하여 80이라는 값을 정지 상태로 보여주기로 한다
        progressBar.setIndeterminate(false);
        progressBar.setProgress(80);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                dialog = new ProgressDialog(MainActivity.this);
                dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
                dialog.setMessage("데이터를 확인하는 중입니다.");

                dialog.show();
            }
        });

        Button button2 = findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                //프로그레스 대화상자 없애기
                if(dialog != null){
                    dialog.dismiss();
                }
            }
        });
    }
}





스피너가 계속 돌아가면서 프로그레스 다이얼로그를 보여준다.
또한 상단에 빨간 선으로 80까지 상태 값이 표시되고 있다.
이 코드에서는 닫기 버튼으로 프로그레스 대화상자를 없앨수 없다.