[Android] Android sqlight database usage(안드로이드 sqlight 데이터베이스 사용)

Android sqlight database usage(안드로이드 sqlight 데이터베이스 사용)


데이터베이스(Databse, db)는 체계화된 데이터의 모임이다. 논리적으로 연관된 하나 이상의 자료의 모음으로 그 내용을 고도로 구조화함으로써 검색과 갱신의 효율화를 꾀한 것이다. 즉, 몇 개의 자료 파일을 조직적으로 통합하여 자료 항목의 중복을 없애고 자료를 구조화하여 기억시켜 놓은 자료의 집합체라고 할 수 있다. 자세한 데이터베이스에 대한 내용은 databse wiki를 참고하자.

안드로이드에서도 데이터를 관리하기 위해서 db를 사용하면 좋다. 이 글에서는 안드로이드에서 사용하기 편리한 sqlight를 사용하여 데이터를 관리하는 방법에 대해서 소개한다.

1. DB 관리 클레스 생성 (DBHelper)

Activity에서 DB를 잘 관리하기 위해서 DB를 관리하기 위한 DBHelper라는 클래스를 생성한다. 아래 예제코드에서는 1개의 앱에서는 1개의 db파일만을 관리하기 때문에 생성자에서 db파일을 생성해서 관리한다. 만약에 사용자별로 db를 관리하거나 생성하고자 한다면 생성자에서 db파일을 생성하지 않고 추가 생성관련 함수에서 db를 생성하면 된다.

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.time.LocalDateTime;

public class DBHelper extends SQLiteOpenHelper {

    Context context;
    MainActivity activity;
    String tb_name;
    int version;
    SQLiteDatabase.CursorFactory factory;

    // DBHelper 생성자로 관리할 DB 이름과 버전 정보를 받음
    public DBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        this.context = context;
        tb_name = name;
        this.version = version;
        this.factory = factory;
        SQLiteDatabase db = getWritableDatabase();
        db.execSQL("CREATE TABLE IF NOT EXISTS " + name + " (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                "time DATETIME DEFAULT (datetime('now','localtime')), patient_name VARCHAR2(100), patient_age INT, patient_gender INT, " +
                "patient_number INT, patient_time INT, ave_speed DOUBLE, physiological_age DOUBLE, fraility_index DOUBLE," +
                "raw_file_path VARCHAR2(100));");
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }

	//데이터 베이스 데이터의 갯수를 출력
    public long getDataSize() {
        SQLiteDatabase db = this.getReadableDatabase();
        long count = DatabaseUtils.queryNumEntries(db, tb_name);
        db.close();
        return count;
    }

	//새로운 데이터 입력
    public void insert(String patient_name, int patient_age, int patient_gender, int patient_number, int patient_time, double ave_speed, double physiological_age, double fraility_index, String raw_file_path) {
        // 읽고 쓰기가 가능하게 DB 열기
        SQLiteDatabase db = getWritableDatabase();
        // DB에 입력한 값으로 행 추가
        ContentValues values = new ContentValues();
        values.put("patient_name", patient_name);
        values.put("patient_age", patient_age);
        values.put("patient_gender", patient_gender);
        values.put("patient_number", patient_number);
        values.put("patient_time", patient_time);
        values.put("ave_speed", ave_speed);
        values.put("physiological_age", physiological_age);
        values.put("fraility_index", fraility_index);
        values.put("raw_file_path", raw_file_path);
        db.insert(tb_name, null, values);

        db.close();
    }

	//기존에 존재하는 데이터 업데이트
    public void update(int id, String patient_name, int patient_age, int patient_gender, int patient_number, int patient_time, double ave_speed, double physiological_age, double fraility_index, String raw_file_path) {
        // 읽고 쓰기가 가능하게 DB 열기
        SQLiteDatabase db = getWritableDatabase();
        // DB에 입력한 값으로 행 추가
        ContentValues values = new ContentValues();
        values.put("patient_name", patient_name);
        values.put("patient_age", patient_age);
        values.put("patient_gender", patient_gender);
        values.put("patient_number", patient_number);
        values.put("patient_time", patient_time);
        values.put("ave_speed", ave_speed);
        values.put("physiological_age", physiological_age);
        values.put("fraility_index", fraility_index);
        values.put("raw_file_path", raw_file_path);
        db.update(tb_name, values, "_id=?", new String[]{String.valueOf(id)});
        db.close();
    }
	
	//특정 id를 갖는 데이터를 json으로 출력
    public JSONObject getData(int id) {
        Log.i("ID", String.valueOf(id));
        SQLiteDatabase db = getReadableDatabase();
        String query = "SELECT * FROM " + tb_name + " WHERE _id = ?";
        Cursor cursor = db.rawQuery(query, new String[]{String.valueOf(id)});
        cursor.moveToPosition(0);

        int totalColumn = cursor.getColumnCount();
        JSONObject rowObject = new JSONObject();

        for (int i = 0; i < totalColumn; i++) {
            if (cursor.getColumnName(i) != null) {
                try {
                    if (cursor.getString(i) != null) {
                        rowObject.put(cursor.getColumnName(i), cursor.getString(i));
                    } else {
                        rowObject.put(cursor.getColumnName(i), "");
                    }
                } catch (Exception e) {
                
                }
            }
        }
        cursor.close();
        db.close();
        return rowObject;
    }

	//시작 id부터 일정 갯수의 데이터를 json array로 얻어온다
    public JSONArray getResult(int start_index, int number_of_data) {
        // 읽기가 가능하게 DB 열기
        SQLiteDatabase db = getReadableDatabase();
        String result = "";

        // DB에 있는 데이터를 쉽게 처리하기 위해 Cursor를 사용하여 테이블에 있는 모든 데이터 출력
        Cursor cursor = db.rawQuery("SELECT * FROM " + tb_name, null);
        JSONArray resultSet = new JSONArray();
        cursor.moveToPosition(start_index);
        int index = 0;
        while (index < number_of_data) {

            int totalColumn = cursor.getColumnCount();
            JSONObject rowObject = new JSONObject();

            for (int i = 0; i < totalColumn; i++) {
                if (cursor.getColumnName(i) != null) {
                    try {
                        if (cursor.getString(i) != null) {
                            rowObject.put(cursor.getColumnName(i), cursor.getString(i));
                        } else {
                            rowObject.put(cursor.getColumnName(i), "");
                        }
                    } catch (Exception e) {

                    }
                }

            }
            resultSet.put(rowObject);
            cursor.moveToNext();
            index = index + 1;
        }

        cursor.close();
        return resultSet;
    }

	//최근 데이터로 부터 일정 갯수의 데이터를 json array 로 얻어온다.
    public JSONArray getRecentData(int number_of_data) {
        // 읽기가 가능하게 DB 열기
        SQLiteDatabase db = getReadableDatabase();
        String result = "";

        // DB에 있는 데이터를 쉽게 처리하기 위해 Cursor를 사용하여 테이블에 있는 모든 데이터 출력
        Cursor cursor = db.rawQuery("SELECT * FROM " + tb_name, null);
        JSONArray resultSet = new JSONArray();
        cursor.moveToLast();
        int index = 0;
        while (index < number_of_data) {

            int totalColumn = cursor.getColumnCount();
            JSONObject rowObject = new JSONObject();

            for (int i = 0; i < totalColumn; i++) {
                if (cursor.getColumnName(i) != null) {
                    try {
                        if (cursor.getString(i) != null) {
                            rowObject.put(cursor.getColumnName(i), cursor.getString(i));
                        } else {
                            rowObject.put(cursor.getColumnName(i), "");
                        }
                    } catch (Exception e) {

                    }
                }

            }
            resultSet.put(rowObject);
            cursor.moveToPrevious();
            index = index + 1;
        }

        cursor.close();
        return resultSet;
    }

	//Primary key인 id를 이용해서 검색 후 삭제
    public void delete(int item) throws JSONException {

        // 입력한 항목과 일치하는 행 삭제
        SQLiteDatabase db = getWritableDatabase();
        db.execSQL("DELETE FROM " + tb_name + " WHERE _id= " + item + ";");
        db.close();
    }
    
	//모든 데이터 삭제
    public void removeAll() {
        SQLiteDatabase db = getReadableDatabase(); // helper is object extends SQLiteOpenHelper
        db.execSQL("DROP TABLE " + tb_name);
        db.execSQL("CREATE TABLE IF NOT EXISTS " + tb_name + " (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                "time DATETIME DEFAULT (datetime('now','localtime')), patient_name VARCHAR2(100), patient_age INT, patient_gender INT, " +
                "patient_number INT, patient_time INT, ave_speed DOUBLE, physiological_age DOUBLE, fraility_index DOUBLE," +
                "raw_file_path VARCHAR2(100));");

    }

}

중요한 부분만 발췌해서 설명하면 다음과 같다.

DBHelper 클래스의 생성자이다. 생성자의 입력으로 들어온 table의 이름으로 db table을 생성하며, db table이 없을때에만 새로 생성한다. primary key로는 id가 되며 데이터가 입력될 때마다 1씩 자동으로 증가한다.

public DBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
	super(context, name, factory, version);
	this.context = context;
	tb_name = name;
	this.version = version;
	this.factory = factory;
	SQLiteDatabase db = getWritableDatabase();
	db.execSQL("CREATE TABLE IF NOT EXISTS " + name + " (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
			"time DATETIME DEFAULT (datetime('now','localtime')), patient_name VARCHAR2(100), patient_age INT, patient_gender INT, " +
			"patient_number INT, patient_time INT, ave_speed DOUBLE, physiological_age DOUBLE, fraility_index DOUBLE," +
			"raw_file_path VARCHAR2(100));");
}

데이터를 추가하는 함수이다. ContentValues에 입력된 데이터를 추가 후 insert 해준다.

public void insert(String patient_name, int patient_age, int patient_gender, int patient_number, int patient_time, double ave_speed, double physiological_age, double fraility_index, String raw_file_path) {
	// 읽고 쓰기가 가능하게 DB 열기
	SQLiteDatabase db = getWritableDatabase();
	// DB에 입력한 값으로 행 추가
	ContentValues values = new ContentValues();
	values.put("patient_name", patient_name);
	values.put("patient_age", patient_age);
	values.put("patient_gender", patient_gender);
	values.put("patient_number", patient_number);
	values.put("patient_time", patient_time);
	values.put("ave_speed", ave_speed);
	values.put("physiological_age", physiological_age);
	values.put("fraility_index", fraility_index);
	values.put("raw_file_path", raw_file_path);
	db.insert(tb_name, null, values);
	db.close();
}

기존에 있던 데이터를 변경해 준다. 이때는 어떤 데이터를 수정할 지 id 값을 입력해 줘야 한다.

public void update(int id, String patient_name, int patient_age, int patient_gender, int patient_number, int patient_time, double ave_speed, double physiological_age, double fraility_index, String raw_file_path) {
	// 읽고 쓰기가 가능하게 DB 열기
	SQLiteDatabase db = getWritableDatabase();
	// DB에 입력한 값으로 행 추가
	ContentValues values = new ContentValues();
	values.put("patient_name", patient_name);
	values.put("patient_age", patient_age);
	values.put("patient_gender", patient_gender);
	values.put("patient_number", patient_number);
	values.put("patient_time", patient_time);
	values.put("ave_speed", ave_speed);
	values.put("physiological_age", physiological_age);
	values.put("fraility_index", fraility_index);
	values.put("raw_file_path", raw_file_path);
	db.update(tb_name, values, "_id=?", new String[]{String.valueOf(id)});
	db.close();
}

특정 id의 데이터를 json파일로 출력한다. json파일은 구조화된 데이터를 갖는 객체이다.

//특정 id를 갖는 데이터를 json으로 출력
public JSONObject getData(int id) {
	SQLiteDatabase db = getReadableDatabase();
	String query = "SELECT * FROM " + tb_name + " WHERE _id = ?";
	Cursor cursor = db.rawQuery(query, new String[]{String.valueOf(id)});
	cursor.moveToPosition(0);

	int totalColumn = cursor.getColumnCount();
	JSONObject rowObject = new JSONObject();

	for (int i = 0; i < totalColumn; i++) {
		if (cursor.getColumnName(i) != null) {
			try {
				if (cursor.getString(i) != null) {
					rowObject.put(cursor.getColumnName(i), cursor.getString(i));
				} else {
					rowObject.put(cursor.getColumnName(i), "");
				}
			} catch (Exception e) {
			
			}
		}
	}
	cursor.close();
	db.close();
	return rowObject;
}

시작 id에서 부터 일정한 갯수의 데이터를 얻어오는 함수이다.

//시작 id부터 일정 갯수의 데이터를 json array로 얻어온다
public JSONArray getResult(int start_index, int number_of_data) {
	// 읽기가 가능하게 DB 열기
	SQLiteDatabase db = getReadableDatabase();
	String result = "";

	Cursor cursor = db.rawQuery("SELECT * FROM " + tb_name, null);
	JSONArray resultSet = new JSONArray();
	cursor.moveToPosition(start_index);
	int index = 0;
	while (index < number_of_data) {

		int totalColumn = cursor.getColumnCount();
		JSONObject rowObject = new JSONObject();

		for (int i = 0; i < totalColumn; i++) {
			if (cursor.getColumnName(i) != null) {
				try {
					if (cursor.getString(i) != null) {
						rowObject.put(cursor.getColumnName(i), cursor.getString(i));
					} else {
						rowObject.put(cursor.getColumnName(i), "");
					}
				} catch (Exception e) {

				}
			}

		}
		resultSet.put(rowObject);
		cursor.moveToNext();
		index = index + 1;
	}
	cursor.close();
	return resultSet;
}

가장 최근 데이터 (가장 큰 id를 갖는 데이터)로 부터 일정 갯수의 데이터를 출력하는 함수

//최근 데이터로 부터 일정 갯수의 데이터를 json array 로 얻어온다.
public JSONArray getRecentData(int number_of_data) {
	// 읽기가 가능하게 DB 열기
	SQLiteDatabase db = getReadableDatabase();
	String result = "";

	// DB에 있는 데이터를 쉽게 처리하기 위해 Cursor를 사용하여 테이블에 있는 모든 데이터 출력
	Cursor cursor = db.rawQuery("SELECT * FROM " + tb_name, null);
	JSONArray resultSet = new JSONArray();
	cursor.moveToLast();
	int index = 0;
	while (index < number_of_data) {

		int totalColumn = cursor.getColumnCount();
		JSONObject rowObject = new JSONObject();

		for (int i = 0; i < totalColumn; i++) {
			if (cursor.getColumnName(i) != null) {
				try {
					if (cursor.getString(i) != null) {
						rowObject.put(cursor.getColumnName(i), cursor.getString(i));
					} else {
						rowObject.put(cursor.getColumnName(i), "");
					}
				} catch (Exception e) {

				}
			}

		}
		resultSet.put(rowObject);
		cursor.moveToPrevious();
		index = index + 1;
	}

	cursor.close();
	return resultSet;
}

특정 id를 갖는 데이터만 삭제, 이렇게 데이터가 삭제된 후 데이터가 새로 입력되어도 삭제된 데이터의 id는 채워지지 않고 계속 id값은 증가한다.

//Primary key인 id를 이용해서 검색 후 삭제
public void delete(int item) throws JSONException {

	// 입력한 항목과 일치하는 행 삭제
	SQLiteDatabase db = getWritableDatabase();
	db.execSQL("DELETE FROM " + tb_name + " WHERE _id= " + item + ";");
	db.close();
}

table에 있는 모든 데이터를 삭제 후 테이블 재 생성. 이런경우에 새로 데이터가 입력되면 id는 처음부터 새로 시작한다.

//모든 데이터 삭제, 
public void removeAll() {
	SQLiteDatabase db = getReadableDatabase(); // helper is object extends SQLiteOpenHelper
	db.execSQL("DROP TABLE " + tb_name);
	db.execSQL("CREATE TABLE IF NOT EXISTS " + tb_name + " (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
			"time DATETIME DEFAULT (datetime('now','localtime')), patient_name VARCHAR2(100), patient_age INT, patient_gender INT, " +
			"patient_number INT, patient_time INT, ave_speed DOUBLE, physiological_age DOUBLE, fraility_index DOUBLE," +
			"raw_file_path VARCHAR2(100));");

}

DBHelper 사용

Mainactivity에 DBHelper 객체를 선언하고 생성한다.

DBHelper user_db;
user_db = new DBHelper(this, "user_db", null, 1);

객체를 생성한 후에 객채의 맴버 함수를 사용하여 입력, 검색, 삭제 등을 수행하면 된다.

다음은 db로부터 데이터를 json 객체로 받아오는 과정이다.

JSONObject data = user_db.getData(data_index);
try {
	time = data.getString("time");
	patient_name = data.getString("patient_name");
	patient_age = data.getInt("patient_age");
	patient_gender = data.getInt("patient_gender");
	patient_number = data.getInt("patient_number");
	patient_average_speed = data.getDouble("ave_speed");
	rawfile = data.getString("raw_file_path");
} catch (JSONException e) {
e.printStackTrace();
}



[Android] Android soft keyboard option (안드로이드 키보드 옵션, 자동 키보드 열림 방지)

Soft keyboard 옵션


이 글에서는 안드로이드의 soft keyboard를 조절하는 옵션에 대해서 설명한다.

만약 한 fragment에 edittext가 존재할 때, 그 fragment로 transition이 수행되면 자동으로 edittext에 입력을 할 수 있도록 keyboard 가 열린다.

하지만 사용자가 입력을 원하는 순간에만 키보드가 열리게 할 필요가 있다 이럴때 soft keyboard에 대한 옵션을 조절하면 된다.

옵션을 조절하기 위해서는 다음과 같이 옵션을 추가해준다.

1. AndroidManifest.xml에 옵션추가

AndroidManifest.xml의 activity옵션에 다음과 같은 옵션을 추가해준다.

android:windowSoftInputMode="stateAlwaysHidden"

즉 전체 activity에 대한 설정을 보면

<activity
    android:name=".MainActivity"
    android:label="@string/app_name"
    android:windowSoftInputMode="stateAlwaysHidden"
    android:configChanges="orientation|screenSize"
    android:theme="@style/AppTheme.NoActionBar">
</activity>

위와 같이 된다.

2. 옵션에 대한 설명

개인적으로 fragment전환 시 키보드가 올라오는 것을 방지하기 위해 stateAlwaysHidden 옵션을 사용하였다.


[Android] Android number picker with dialog (안드로이드 dialog를 이용한 number picker 입력 받기)

number picker with dialog (dialog를 이용한 number picker 입력받기)


이번 글에서는 사용자에게 숫자를 입력받기 위한 방법중에서 number picker를 사용하는 방법에 대해서 알아본다.

숫자를 입력받는 방법으로는 edittext에 직접 키보드로 숫자 입력을 받는 방법이 있다.

하지만 앱을 단순하고 유저 관점에서 사용성이 좋게 하기 위해서는 직접 입력하는 것보다 클릭으로 가능하도록 처리하는 것이 좋다.

따라서 숫자를 아래 그림과 같이 직접 입력이 아닌 입력 가능한 숫자 중에서 선택하도록 만드는 것이 좋다.

number picker

위 예시는 특정 값에 해당하는 설정 버튼을 눌렀을 때 숫자를 선택할 수 있는 dialog가 팝업으로 뜨고 사용자가 숫자를 선택 후 확인을 누르면 적용이 되는 방식이다.

이러한 코드를 구현해 보자.

1. NumberpickerDialog 클래스 구현

아래 코드는 NumberpickrDialog를 구현한 클래스이다. 각 코드마다 기능들은 주석으로 추가해 놓았다.

import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.widget.NumberPicker;

public class NumberPickerDialog extends DialogFragment {
    private NumberPicker.OnValueChangeListener valueChangeListener;

    String title;	//dialog 제목
    String subtitle;	//dialog 부제목
    int minvalue;	//입력가능 최소값
    int maxvalue;	//입력가능 최대값
    int step;	//선택가능 값들의 간격
    int defvalue;	//dialog 시작 숫자 (현재값)

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        final NumberPicker numberPicker = new NumberPicker(getActivity());

		 //Dialog 시작 시 bundle로 전달된 값을 받아온다
		 
        title = getArguments().getString("title");
        subtitle = getArguments().getString("subtitle");
        minvalue = getArguments().getInt("minvalue");
        maxvalue = getArguments().getInt("maxvalue");
        step = getArguments().getInt("step");
        defvalue = getArguments().getInt("defvalue");

		 //최소값과 최대값 사이의 값들 중에서 일정한 step사이즈에 맞는 값들을 배열로 만든다.
        String[] myValues = getArrayWithSteps(minvalue, maxvalue, step);
        
        //displayedvalues를 사용하지 않고 min/max 값을 설정해서 선택을 받을 때는 선택한 보여지는 값이 바로 리턴되는 값이다. 
        //하지만 이런경우에는 보여지는 값은 최소값과 최대값사이에 값이 1씩 증가되는 값들이 모두 보여지게 된다. (별도의 step을 줄 수 없다)
        //일정한 간격의 숫자만을 선택할 수 있게 하려면, String 배열에 display가 되는 값들을 입력해서 setDisplayedValues함수로 입력해줘야 하며
        //이런경우 그 숫자가 리턴값이 아닌 배열의 인덱스가 리턴값이 된다. 따라서 minValue와 maxvalue는 이에 맞게 설정해 주어야 한다.
        
        numberPicker.setMinValue(0);
        numberPicker.setMaxValue((maxvalue - minvalue) / step);
        numberPicker.setDisplayedValues(myValues);
        
        //현재값 설정 (dialog를 실행했을 때 시작지점)
        numberPicker.setValue((defvalue - minvalue) / step);
        //키보드 입력을 방지
        numberPicker.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);

        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        //제목 설정
        builder.setTitle(title);
        //부제목 설정
        builder.setMessage(subtitle);

		 //Ok button을 눌렀을 때 동작 설정
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
            
				 //dialog를 종료하면서 값이 변했다는 함수는 onValuechange함수를 실행시킨다. 
				 //실제 구현에서는 이 클레스의 함수를 재 정의해서 동작을 수행한다.
				 
                valueChangeListener.onValueChange(numberPicker,
                        numberPicker.getValue(), numberPicker.getValue());
            }
        });

		 //취소 버튼을 눌렀을 때 동작 설정
        builder.setNegativeButton("CANCEL", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {

            }
        });

        builder.setView(numberPicker);
		//number picker 실행
        return builder.create();
    }

	//Listener의 getter
    public NumberPicker.OnValueChangeListener getValueChangeListener() {
        return valueChangeListener;
    }

	//Listener의 setter
    public void setValueChangeListener(NumberPicker.OnValueChangeListener valueChangeListener) {
        this.valueChangeListener = valueChangeListener;
    }

	//최소값부터 최대값가지 일정 간격의 값을 String 배열로 출력
    public String[] getArrayWithSteps(int min, int max, int step) {

        int number_of_array = (max - min) / step + 1;

        String[] result = new String[number_of_array];

        for (int i = 0; i < number_of_array; i++) {
            result[i] = String.valueOf(min + step * i);
        }
        return result;
    }
}

이 코드에서 주의해야 할 점은 다음과 같다.

1. 일정 간격의 입력값을 입력 후보군으로 보여주고 싶을 때는 setDisplayedValues 함수를 사용한다

일반적인 numberpicker는 최소값부터 최대값까지 1의 간격의 숫자를 보여주고 선택할 수 있다. 이런 경우에는 Dialog에서 사용자가 선택한 값이 바로 리턴값이 된다.

하지만 최소값부터 최대값까지 일정한 step 간격의 숫자를 보여주고 선택하게 하고 싶다면 (맨 위에 그림처럼) setDisplayedValues함수를 이용해야 한다.

setDisplayedValues 함수는 사용자에게 보여주고자 하는 숫자들을 String 배열로 입력받는다. Dialog는 입력받은 숫자들의 String 값을 사용자에게 보여준다.

이때 주의해야 할 점은 사용자가 선택한 숫자에 대한 리턴값은 String 배열의 index이다. 즉 리턴을 받았을 때 실제 사용자가 선택한 값은 index를 이용해서 다시 계산해야 한다는 것이다.

또한 이때 minvalue와 maxvalue또한 String 배열의 index에 맞게 설정해 줘야 한다.

2. 외부 클레스에서 Implements를 이용해서 Listener를 등록해서 사용한다

사용자가 숫자를 선택하고 적용을 시키기 위해 OK 버튼을 누르면 valueChangeListener.onValueChange(numberPicker,numberPicker.getValue(), numberPicker.getValue()) 함수가 실행된다. 이 함수는 외부에서 implement로 상속한 함수의 onValueChange함수를 실행시킨다. 이와같은 역할을 하는 코드는 다음과 같다.

//구현하고자 하는 fragment는 implements로 NumberPicker.OnValueChangeListener 를 받는다
public class FragmentDeviceSetting extends Fragment implements NumberPicker.OnValueChangeListener{

	//중간 내용 생략


	//valueChangeListener.onValueChange가 수행되었을 때 수행될 함수
    @Override
    public void onValueChange(NumberPicker numberPicker, int i, int i1) {
		int setting_value = activity.sensor_minimum_distance + numberPicker.getValue()*activity.distance_setting_step;

		//원하는 동작 수행
	}

	//Dialog를 시작하는 함수, 특정 버튼을 눌렀을 때 이 함수를 실행 시키면 된다
    public void showNumberPicker(View view, String title, String subtitle, int maxvalue, int minvalue, int step, int defvalue){
        NumberPickerDialog newFragment = new NumberPickerDialog();

		//Dialog에는 bundle을 이용해서 파라미터를 전달한다
        Bundle bundle = new Bundle(6); // 파라미터는 전달할 데이터 개수
        bundle.putString("title", title); // key , value
        bundle.putString("subtitle", subtitle); // key , value
        bundle.putInt("maxvalue", maxvalue); // key , value
        bundle.putInt("minvalue", minvalue); // key , value
        bundle.putInt("step", step); // key , value
        bundle.putInt("defvalue", defvalue); // key , value
        newFragment.setArguments(bundle);
        //class 자신을 Listener로 설정한다
        newFragment.setValueChangeListener(this);
        newFragment.show(activity.getFragmentManager(), "number picker");
    }

}

위에 주석으로 실제 Dialog를 실행하는 fragment코드까지 추가하였다. 주석을 보면서 이해해보면 그렇게 어렵지 않음을 알 수 있다. 하지만 여기서 주의해야 할 점은 numberPicker.getValue()로 얻어지는 값이 실제 사용자에게 보여지는 값이 아닌 String 배열에 들어가 있는 값들의 index값이라는 점이다. 따라서 얻어지는 index값을 이용해서 실제 사용자가 선택한 값을 추정해야 한다.

int setting_value = sensor_minimum_distance + numberPicker.getValue()* distance_setting_step;

위의 예제 코드에서는 String 배열을 만들때 과정을 이용해서 역으로 계산하였다.

위의 코드들은 실제 사용하고 있는 코드에서 필요한 부분만 발췌하여 작성하였기 때문에 저 코드만으로는 동작하지 않을 수 있다. 코드를 이해하고 본인의 코드에 맞게 응용하면 된다.


[Android] Android configChanges option (안드로이드 화면 회전시 view 유지방법)

How to maintain view when screen direction change using configChanges option


안드로이드의 경우 화면이 회전되었을 경우 화면을 종료시키고 새로운 layout으로 재 시작한다.

즉 세로화면 -> 가로화면으로 전환 시 onDestroy() 함수가 호출되고 가로모드에서 다시 onCreate() 함수가 호출된다.

즉 환경변화가 일어날 경우 기본적인 동작은 activity 의 재시작이다.

이럴때 AndroidManifest.xml 파일에 ` android:configChanges`을 설정함으로써 activity가 reset되는것을 막을 수 있다.

이런경우 activity의 onDestroy()onCreate() 함수 대신에 onConfigurationChanged()함수가 호출된다.

이러한 옵션이 왜 필요한지 예를 들어보자.

사용자의 이름을 입력하는 Edittext를 1개 포함하는 화면이 있다고 하자.

화면이 처음 구성되는 onCreate()함수에서 사용자의 이름변수를 null로 초기화 하고, 화면이 구성된 이후에 사용자가 이름을 입력하였다.

만약 이때 화면이 가로모드에서 세로모드로 변경되면 화면이 종료되고 다시 시작되기 때문에 사용자의 이름변수는 다시 null이 입력되어 있게 된다.

이런 상황에서 원하는 동작은 사용자가 이름을 입력하였기 때문에 Edittext는 사용자가 입력된 내용을 그대로 유지하고 있어야 한다. 이럴때 configChanges 옵션중에서 orientation 옵션을 주면 화면의 방향이 변경되어도 초기화 되지 않는다.

적용시키는 방법은 다음과 같다.

1. AndroidManifest.xml 수정

AndroidManifest.xml 파일에 android:configChanges="orientation" 을 추가해준다. activity안에 넣어주어야 한다. 다른 설정과 함께 작성하면 다음과 같다.

 <activity
     android:name=".MainActivity"
     android:label="@string/app_name"
     android:windowSoftInputMode="stateAlwaysHidden"
     android:configChanges="orientation"
     android:theme="@style/AppTheme.NoActionBar">
 </activity>

위의 옵션은 화면의 방향이 변해도 화면이 초기화 되지 않도록 하며 (android:configChanges="orientation"), fragment가 변경되었을 때 항상 soft 키보드를 숨기는 옵션 (android:windowSoftInputMode="stateAlwaysHidden") 을 적용하였다. 키보드 관련된 내용은 다음 글을 참고하면 된다.

2. onConfigurationChanged() 함수 작성

1번과 같이 옵션을 추가하고 화면의 방향이 변경 시, onConfigurationChanged() 함수가 호출된다. 만약 화면의 방향이 전환되었을 때 수행해야 하는 작업이 있는 경우 이 함수에 추가하면 된다.

3. configChanges의 여러가지 옵션들

참고사이트: https://aroundck.tistory.com/36

위와 같이 다양한 옵션을 사용할 수 있다. 위의 옵션은 or 구분자(|)를 통해 다음과 같이 중복 입력 가능하다

android:configChanges="orientation|screenLayout"



[Android] Android fragment transition with animation (안드로이드 프레그먼트 애니메이션 화면전환)

Fragment transition with animation


이 글에서는 fragment의 화면전환 할 때 애니메이션을 넣는 방법에 대해서 설명한다.

1. 애니메이션 정의

화면전환에 대한 애니메이션을 각각 정의해 주어야 한다.

애니메이션을 정의하는 경로는 res/anim/파일명.xml이다 .

다양한 동작을 하는 애니메이션들을 정의해보자

enter_from_left.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"  >
    <objectAnimator
    android:propertyName="x"
    android:duration="600"
    android:valueFrom="-400dp"
    android:valueTo="0"
    android:valueType="floatType" />
</set>

enter_from_right.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <objectAnimator
    android:duration="600"
    android:propertyName="x"
    android:valueFrom="400dp"
    android:valueTo="0"
    android:valueType="floatType" />
</set>

exit_to_left.xml

<?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android" >
    <objectAnimator
    android:duration="600"
    android:propertyName="x"
    android:valueFrom="0"
    android:valueTo="-400dp"
    android:valueType="floatType" />
</set>

exit_to_left.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
    android:duration="600"
    android:propertyName="x"
    android:valueFrom="0"
    android:valueTo="400dp"
    android:valueType="floatType" />
</set>

모든 동작은 좌우로 fragment를 이동하는 애니메이션이며 property의 이름은 x 이다.

이동하는 애니메이션 뿐만 아니라 fade_in, fade_out 애니메이션도 다음과 같이 정의할 수 있다.

fade_in.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"  >
    <objectAnimator
    android:duration="600"
    android:propertyName="alpha"
    android:valueFrom="0"
    android:valueTo="1"
    android:valueType="floatType"
    />
</set>

fade_out.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"  >
    <objectAnimator
    android:duration="600"
    android:propertyName="alpha"
    android:valueFrom="1"
    android:valueTo="0"
    android:valueType="floatType"
    />
</set>

2. 애니메이션을 통한 Fragment 전환

애니메이션을 적용한 fragment의 전환을 위해서는 기본적은 fragment transition에 setCustomAnimations 옵션을 추가해주면 된다. 다음은 이전 화면은 왼쪽으로 나가면서 오른쪽에서 새로운 fragment가 들어오는 애니메이션이 적용되는 예이다.

FragmentManager manager = getFragmentManager();
manager.beginTransaction()
    .setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left,R.anim.exit_to_right)
    .replace(R.id.content_main, fragment)
    .commit();

위의 예제는 뒤로가기를 눌렀을 때 이전 fragment로 가지 않도록 stack에 누적하지 않는 코드이다. 만약 뒤로가기를 눌렀을 때 다시 이전 화면으로 돌아가는 코드로 만들고 싶다면 transaction.addToBackStack(null) 를 추가해주면 된다.

setCustomAnimations의 인자는 총 4개이다.

첫번째 인자는 fragment의 transition이 수행되었을 때 새로운 fragment의 애니메이션이며 두번째 인자는 기존에 화면에 있던 fragment의 애니메이션이다.

즉 새로운 fragment는 오른쪽에서 들어오면서 기존에 있던 fragment는 왼쪽으로 나간다.

세번째 인자는 뒤로가기를 눌렀을 때, 즉 이전화면으로 이동 시의 애니메이션이다.

즉 stack에 쌓여있던 fragment는 왼쪽에서 들어오며, 기존에 있던 fragment는 오른쪽으로 사라진다.


Pagination