블로그 이미지
매몰

모바일 어플리케이션 개발 1인 기업 고영진모바일입니다. 저와 함께 맛깔스러운 앱을 만들어 보아요~

Rss feed Tistory
개발/android 2019.01.17 17:27

EditText에 자동으로 단위 콤마 넣기



NumberFormat 을 이용하면 숫자에 단위 콤마를 쉽게 찍을수 있다.


하지만 이를 EditText에 적용할려고 하면 한가지 문제가 생긴다.


입력할때마다 콤마가 찍혀야 하는데...

addTextChangedListener에서 NumberFormat으로 콤마를 찍으면 당연히 텍스트가 변경되므로 다시 리스너가 호출된다. 즉, 호출이 무한 반복되면서 앱이 멈추게 된다.


그래서 살짝 꼼수를 부려봤다.


EditText 밑에 같은 크기의 TextView를 깔고 여기에 콤마를 찍은 텍스트를 대신 써주고,

후에 EditText를 동기화 하는 것이다. 물론, EditText는 투명하게 설정해 안보이게 한다.


사실 임시 방편으로 만든것인데.. 생각보다 잘 작동해 계속 쓰고 있다 ㅎㅎㅎ



소스는 다음과 같다. 요즘 뜨고 있다는 코틀린으로 작성했다~


class NumberEditText: RelativeLayout {

private var editText: EditText
private var textView: TextView

init {
editText = EditText(context)
textView = TextView(context)

initBase()
}

constructor(context: Context): super(context) {
editText = EditText(context)
textView = TextView(context)

initBase()
}

private fun initBase() {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)

//EditText가 안보이도록 투명 처리한다
editText.setTextColor(Color.TRANSPARENT)
editText.setBackgroundColor(Color.TRANSPARENT)
editText.gravity = gravity

textView.gravity = gravity

//TextView 위에 EditText를 생성한다
addView(textView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
addView(editText, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))

editText.setOnTouchListener { view, motionEvent ->
//EditText를 터치하면 TextView의 내용을 동기화 한다
if (motionEvent.action == MotionEvent.ACTION_UP) {
//내용을 쓸때 입력 커서 위치가 변경되지 않도록 조정한다
val length = textView!!.text.length - (view as EditText).text.length
val selectionStart = view.selectionStart + length
val selectionEnd = view.selectionEnd + length

//내용을 쓰고 커서 위치를 조정한 후 적용되도록 포커스를 요청한다
view.setText(textView?.text)
view.setSelection(selectionStart, selectionEnd)
view.requestFocus()
}

false
}

editText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {

}

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {

}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
//콤마를 찍어 TextView에 쓴다
textView.text = getNumberText(s.toString())
}
})
}

private fun getNumberText(text: String): String {
return if (text.length > 1) NumberFormat.getNumberInstance().format(text.replace(",", "").toDouble()) else text
}
}



수제앱장인


 

 

CEO

Developer

S/W Enginner

고영진


실패만 하고 있어도 꿈을 포기하지 않는 남자
제가 직접 경험하고 습득한 지식을 위주로 올릴게요

사업자 정보 표시
고영진모바일 | 고영진 | 서울특별시 관악구 낙성대동 서울대연구공원 SK상생혁신센터 | 사업자 등록번호 : 109-11-82076 | TEL : 010-9990-3674 | Mail : gyjmeba@hanmail.net | 통신판매신고번호 : 2010-서울강서-0217호 | 사이버몰의 이용약관 바로가기
개발/android 2018.09.27 13:30

sqlite의 time이 자정을 인식 못할 경우, 간단한 꼼수 해결법



안드로이드의 sqlite 데이터베이스를 사용하는 도중 이상한 점을 하나 발견했다.


time 데이터타입에서 00시 또는 24시, 즉 자정일때 time() 함수가 먹히질 않는다는 점이다.


예를 들면,

(time 데이터타입의 field1 시각 +  int 데이터타입의 field2 초) > 특정시각


을 비교할때


SELECT * FROM table1 WHERE time(field1, field2 ||' seconds') > 23:59:30

와 같이 사용한다.


하지만 이때, field1이 23:59:00 이고 field2가 60초 이상이면 자정이 넘어가면서 SELECT 결과가 항상 아무것도 안나오게 된다. field1이 24:00:00 이상이어도 마찬가지다.


이걸 해결하기 위해 만방으로 살펴봤지만... 역시 코딩은 꼼수가 최고다.


그냥... 자정이 넘어갈것 같으면 시간을 앞당겨주면 된다ㅎㅎ



다음과 같이 해보자~

private String getTime(String time, String addtime) {
return "time('" + time + "'" + addtime + ")";
}

private String getTime(String field, String addfield, String addtime) {
return "time(" + field + "," + addfield + "||' seconds'" + addtime + ")";
}

private Cursor fetch(SQLiteDatabase db, String time) {
//특정시각 time이 21시보다 크면 3시간을 앞당겨 준다.
String addText = Integer.parseInt(time.substring(0, 2)) < 22 ? "" : ",'-3 hours'";

return db.rawQuery("SELECT * FROM table1 WHERE " + getTime("field1", "field2", addText) + "<" + getTime(time, addText), null);
}

자정이 넘어 갔을때 시간을 앞당겨 비교해 주면 정상적으로 작동한다. 


수제앱장인


 

 

CEO

Developer

S/W Enginner

고영진


실패만 하고 있어도 꿈을 포기하지 않는 남자
제가 직접 경험하고 습득한 지식을 위주로 올릴게요



사업자 정보 표시
고영진모바일 | 고영진 | 서울특별시 관악구 낙성대동 서울대연구공원 SK상생혁신센터 | 사업자 등록번호 : 109-11-82076 | TEL : 010-9990-3674 | Mail : gyjmeba@hanmail.net | 통신판매신고번호 : 2010-서울강서-0217호 | 사이버몰의 이용약관 바로가기
개발/android 2018.06.07 14:03

Intent 전달 방식을 모방하여 부모 activity를 호출하기


IOS에서는 prepare()을 통해서 부모 controller를 쉽게 전달할 수 있다.

하지만 Android에서는 Intent로 activity를 전달하기 쉽지 않다.


뭐, Intent 말고도 여러 전달 방식이 있기 때문에 상관없지만...

깔끔하게 코딩하고 싶은 마음에 함 만들어 보았다.


static을 이용하지만, 마치 Intent로 전달하는것 같은 느낌으로ㅎㅎ



public class BaseActivity extends FragmentActivity {

private static final String KEY_PARAM_CONTEXT_ID = "ParamContextId";

//전달할 Context를 담는 Map
private static HashMap<Long, Context> mParamContextMap = null;

//전달된 Context를 저장할 인스턴스
private Context mParamContext;

//부모 Activity에서 호출되어 Context를 받는다
public static void putParamContext(Intent intent, Context context) {

//Map이 널이면 즉 Context가 하나도 없다면 생성한다
if (mParamContextMap == null)
mParamContextMap = new HashMap<>();

//현재 시간을 식별자로 전달 Intent에 저장한다. 이는 Context가 static에 담아지기 때문에 상속받은 모든 activity에서 만약에 있을 출동을 막기 위함이다.
long id = System.currentTimeMillis();
intent.putExtra(KEY_PARAM_CONTEXT_ID, id);

//위의 식별자를 해당 키로 하여 Map에 Context를 담는다.
mParamContextMap.put(id, context);
}

//부모 Activity의 Context를 반환한다.
protected Context getParamContext() {
return mParamContext;
}

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

//전달된 Context가 있다면 실행한다.
if (mParamContextMap != null) {
long id = getIntent().getLongExtra(KEY_PARAM_CONTEXT_ID, 0);
if (id != 0) {
//Context를 인스턴스에 저장하고 static Map에서는 삭제한다.
mParamContext = mParamContextMap.get(id);
mParamContextMap.remove(id);
}

//Map에 아무것도 없다면 널로 풀어준다.
if (mParamContextMap.size() == 0)
mParamContextMap = null;
}
}
}




부모 Activity에서 자식 Activity 호출...


Intent intent = new Intent(this, MyActivity.class);
BaseActivity.putParamContext(intent, this);
startActivity(intent);



자식 Activity에서 부모 Activity 호출...


public class MyActivity extends BaseActivity {

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

    Context parentContext = getParamContext();

//부모 Context롤 이용하여 작업한다.

//

//

}

}


 

 



수제 앱 장인: 고영진


(주)고영진모바일

1인기업 대표이사 겸 개발자

  

     실패만 하고 있어도 꿈을 포기하지 않는 남자 

     제가 직접 경험하고 습득한 지식을 위주로 올릴게요

 






사업자 정보 표시
고영진모바일 | 고영진 | 서울특별시 관악구 낙성대동 서울대연구공원 SK상생혁신센터 | 사업자 등록번호 : 109-11-82076 | TEL : 010-9990-3674 | Mail : gyjmeba@hanmail.net | 통신판매신고번호 : 2010-서울강서-0217호 | 사이버몰의 이용약관 바로가기
개발/android 2017.09.11 10:17

ListView에 EditText를 넣을때 반드시 살펴봐야 할점

설정 페이지를 만들때 ListView로 EditText를 넣고 싶은 유혹에 빠진다


하지만 ListView의 특성상 사실 EditText는 적합하지 않다.

ListView는 각각의 row뷰를 재사용하여 메모리를 아끼기 때문에 

EditText의 변경값을 일일이 저장해 두었다가 다시 불려줘야 한다.


그리고 이때 진짜 주의할게 하나 있다.

변경 리스너의 중복을 방지하는 것이다. 반드시 재사용 뷰의 예전 리스너를 지워줘야 한다.

안그러면 예전 목록들과 뒤죽박죽 되버린다.



이해를 돕기 위해 리스너를 지우지 않았을때와 지웠을때도 비교해 보았다.


public class MainActivity extends Activity {

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

//리스트 목록 넣기
String[] names = getResources().getStringArray(R.array.list_item);
ArrayList<Item> list = new ArrayList<>();

int length = names.length;
for (int i = 0 ; i < length ; i++)
list.add(new Item(names[i]));

((ListView)findViewById(R.id.ItemListView)).setAdapter(new ItemAdapter(this, R.layout.list_row, list));
}

//리스트 목록 클래스
private class Item {
public String mName, mValue;
public TextWatcher mTextWatcher;

public Item(String name) {
mName = name;
mValue = "";

//EditText 변경 리스너 생성
mTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
//변경된 값을 저장한다
mValue = s.toString();
}

@Override
public void afterTextChanged(Editable s) {

}
};
}
}

private class ItemAdapter extends ArrayAdapter<Item> {

private int mResource;

public ItemAdapter(Context context, int resource, List<Item> list) {
super(context, resource, list);
mResource = resource;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
LinearLayout itemView;

if (convertView == null) {
itemView = new LinearLayout(getContext());

LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
vi.inflate(mResource, itemView, true);
}
else
itemView = (LinearLayout) convertView;

Item item = getItem(position);

((TextView)itemView.findViewById(R.id.NameTextView)).setText(item.mName);

EditText editText = (EditText)itemView.findViewById(R.id.ValueEditText);

//예전 리스너를 삭제한다
clearTextChangedListener(editText);

//값을 불려오고 해당 리스너를 적용한다
editText.setText(item.mValue);
editText.addTextChangedListener(item.mTextWatcher);

return itemView;
}

private void clearTextChangedListener(EditText editText) {
//리스트 목록의 모든 리스너를 대상으로 검사하여 삭제해 준다
int count = getCount();
for (int i = 0 ; i < count ; i++)
editText.removeTextChangedListener(getItem(i).mTextWatcher);
}
}
}



main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent"
android:gravity="center_vertical">

<ListView android:id="@+id/ItemListView"
android:layout_width="match_parent" android:layout_height="match_parent"/>

</LinearLayout>



list_row.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent"
android:padding="10dp" android:gravity="center_vertical">

<TextView android:id="@+id/NameTextView"
android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1"
android:layout_marginRight="5dp"/>

<EditText android:id="@+id/ValueEditText"
android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2"/>

</LinearLayout>



array.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

<string-array name="list_item">
<item>사자</item>
<item>호랑이</item>
<item>토끼</item>
<item>코끼리</item>
<item>강아지</item>
<item>고양이</item>
<item>표범</item>
<item>송아지</item>
<item>돼지</item>
<item>코알라</item>
<item>캥거루</item>
<item>양</item>
<item>염소</item>
<item>늑대</item>
<item>여우</item>
<item>닭</item>
<item>독수리</item>
<item>쥐</item>
<item>뱀</item>
<item>말</item>
<item>곰</item>
<item>두더지</item>
<item>사슴</item>
<item>노루</item>
<item>하마</item>
<item>악어</item>
<item>꿩</item>
<item>비둘기</item>
<item>까마귀</item>
<item>고슴도치</item>
</string-array>

</resources>




clearTextChangedListener() 함수를 주석처리 했을때 (예전 변경 리스너를 지워주지 않았을때)






clearTextChangedListener() 함수를 주석처리 하지 않았을때  (예전 변경 리스너를 지워주었을때)






 

 



수제 앱 장인: 고영진


고영진모바일 1인기업 대표 겸 개발자 

  

     실패만 하고 있어도 꿈을 포기하지 않는 남자 

     제가 직접 경험하고 습득한 지식을 위주로 올릴게요

 


사업자 정보 표시
고영진모바일 | 고영진 | 서울특별시 관악구 낙성대동 서울대연구공원 SK상생혁신센터 | 사업자 등록번호 : 109-11-82076 | TEL : 010-9990-3674 | Mail : gyjmeba@hanmail.net | 통신판매신고번호 : 2010-서울강서-0217호 | 사이버몰의 이용약관 바로가기
개발/android 2017.06.27 11:50

부등호 없는 부등식 소스를 짜보자



반복적으로 또는 사용자 임의로 부등식 비교를 할때 

일일이 부등호를 넣어야 하는 불편이 있다.



예를 들어


int a = 1, b = 5;

String sign = "<"


if (sign.equels("<")) {

return a < b;

}

else if (sign.equels("<=")) {

return  a <= b;

}

else if (sign.equels(">")) {

return  a > b;

}

else if (sign.equels(">=")) {

return a >= b;

}





역시 소스가 깔끔하지 않다.

간단한 수식을 이용해 다음과 같이 해보자.


//부등식 객체 클래스
public static class Compare {
private int mA, mB;
private boolean mRight, mEquel;

public Compare(int a, int b, boolean right, boolean equel) {
mA = a;
mB = b;
mRight = right;
mEquel = equel;
}

public boolean check() {
return check(mA, mB, mRight, mEquel);
}

//a, b: 비교할 숫자
//right: b(오른쪽 숫자)가 커야할때는 true, 그렇지 않을 경우 false
//equel: 등호 포함 여부
public static boolean check(int a, int b, boolean right, boolean equel) {
//숫자가 같을 경우에는 등호 여부가 바로 값이 되고
//숫자가 다를 경우에만 비교값과 부등호를 확인한다
return a == b ? equel : right == a < b;
}
}




//비교 연산자 사용하기

//checks: 부등식 배열
//or: or 이면 true, and 이면 false
private boolean checks(Compare[] checks, boolean or) {
for (int i = 0 ; i < checks.length ; i++) {
//or(true)일 경우에는 하나만 true이면 그대로 리턴
//and(false)일 경우에는 하나만 false이면 그대로 리턴
if (checks[i].check() == or)
return or;
}

//위에서 걸려지지 않았으면 or변수의 반대값 리턴
return !or;
}



위의 예제이다


private void test() {
Log.d("MyLog", "check 1 <= 5: " + Compare.check(1, 5, true, true));
Log.d("MyLog", "check 1 >= 5: " + Compare.check(1, 5, false, true));
Log.d("MyLog", "check 1 < 5: " + Compare.check(1, 5, true, false));
Log.d("MyLog", "check 5 < 1: " + Compare.check(5, 1, true, false));
Log.d("MyLog", "check 5 < 5: " + Compare.check(5, 5, true, false));
Log.d("MyLog", "check 5 <= 5: " + Compare.check(5, 5, true, true));

Compare com1 = new Compare(1, 5, true, true);
Compare com2 = new Compare(23, 17, false, true);
Compare com3 = new Compare(36, 36, false, false);
Compare com4 = new Compare(1, 5, false, true);
Compare com5 = new Compare(23, 17, true, true);
Compare com6 = new Compare(37, 36, false, true);

Log.d("MyLog", "checks (1 <= 5 || 23 >= 17 || 36 > 36): " + checks(new Compare[] {com1, com2, com3}, true));
Log.d("MyLog", "checks (1 <= 5 && 23 >= 17 && 36 > 36): " + checks(new Compare[] {com1, com2, com3}, false));

Log.d("MyLog", "checks (1 >= 5 || 23 <= 17 || 36 > 36): " + checks(new Compare[] {com4, com5, com3}, true));
Log.d("MyLog", "checks (1 <= 5 && 23 >= 17 && 37 > 36): " + checks(new Compare[] {com1, com2, com6}, false));
}



예제의 결과값이다.



D/MyLog: check 1 <= 5: true

D/MyLog: check 1 >= 5: false

D/MyLog: check 1 < 5: true

D/MyLog: check 5 < 1: false

D/MyLog: check 5 < 5: false

D/MyLog: check 5 <= 5: true

D/MyLog: checks (1 <= 5 || 23 >= 17 || 36 > 36): true

D/MyLog: checks (1 <= 5 && 23 >= 17 && 36 > 36): false

D/MyLog: checks (1 >= 5 || 23 <= 17 || 36 > 36): false

D/MyLog: checks (1 <= 5 && 23 >= 17 && 37 > 36): true




 

 



수제 앱 장인: 고영진


고영진모바일 1인기업 대표 겸 개발자 

  

     실패만 하고 있어도 꿈을 포기하지 않는 남자 

     제가 직접 경험하고 습득한 지식을 위주로 올릴게요

 




사업자 정보 표시
고영진모바일 | 고영진 | 서울특별시 관악구 낙성대동 서울대연구공원 SK상생혁신센터 | 사업자 등록번호 : 109-11-82076 | TEL : 010-9990-3674 | Mail : gyjmeba@hanmail.net | 통신판매신고번호 : 2010-서울강서-0217호 | 사이버몰의 이용약관 바로가기
개발/android 2016.11.14 10:47

간단한 코딩으로 큰따음표("")로 묶인 CSV 파일 보기



CSV란 콤마(,)로 구분한 일련의 데이터들을 말한다.

예를 들면 아래와 같다.



고양이,강아지,곰,상어,토끼 


100,20,55,177,83



이런 형식은 서로 호환 되지 않는 프로그램끼리 데이터들을 주고 받을때 유용하게 쓰인다.

또한 매우 간단하기 때문에 부담도 없다.


하지만 흔한 문제점 하나가 있다.

콤마가 있는 데이터가 있을 경우다.



56

10,000

1,500

300

890

2,360,050

74



금액을 표시할때 위와 같이 종종 단위 콤마를 쓰는데

이것을 CSV로 표현하면,



56,10,000,1,500,300,890,2,360,050,74



가 된다. 의도와는 다르게 데이터가 



56

10

100

1

500

300

890

2

360

050

74



로 나누어져 버렸다

그래서 콤마가 있는 데이터를 큰따음표("")로 묶어 버린다



56,"10,000","1,500",300,890,"2,360,050",704



큰따음표로 모두 묶어버리면 좋겠지만 

콤마가 없는 데이터는 그대로 두고 (위에서 56, 300, 890, 704)

콤마가 있는 데이터만 묶는 경우가 있다


우리는 이럴때를 가정해서 살펴볼 것이다.

왜냐하면 보통 이런 경우를 고려하지 않는 뷰어가 꽤 있기 때문이다

당황 하지 말고 코딩인으로써 해결하자~




먼저, 첫번째 큰따음표를 기준으로 나누자. 


private String[] divideText(String text) {
//문자열안에서 첫번째 큰따음표의 위치를 찾는다
int index = text.indexOf("\"");
if (index >= 0) {
//큰따음표가 첫문자 또는 마지막에 있으면 널을 반환하여 알린다
int resultindex = index + 1;
return new String[]{(index > 0 ? text.substring(0, index) : null), (resultindex < text.length() ? text.substring(resultindex) : null)};
}

//찾지 못하면 널을 반환하여 알린다
return null;
}



그 다음은 위의 함수로

큰따음표 안의 문자열과 바깥(앞,뒤) 문자열을 추출하자 


바깥 앞  ""  바깥 뒤

56,"10,000","1,500",300,890,"2,360,050",704


private String[] getText(String text) {
//데이터 문자열을 첫번째 큰따음표로 나눈다
String[] subtexts = divideText(text);
if (subtexts == null) {
//큰따음표가 더이상 없다면
//바깥 앞 문자열 마지막에 콤마를 찍는다
//이는 언제나 문자열 양끝에 콤마가 존재해야 하기 때문이다
if (text.length() > 0)
return new String[] {text + ",", null, null};
}
else {
String[] result = {null, null, null};

//divideText의 앞 문자열을 바깥 앞 문자열로 설정한다
result[0] = subtexts[0];

//divideText의 뒤 문자열은 다시 큰따음표로 나눈다
String[] backtexts = divideText(subtexts[1]);
if (backtexts != null) {
//다시 나누어진 앞 문자열은 큰따음표 안의 문자열
result[1] = backtexts[0];

//뒤 문자열은 큰따음표 바깥 뒤 문자열
result[2] = backtexts[1];
}

return result;
}

return null;
}



자, 이젠 데이터를 추출해 보자


private ArrayList<String> getItemList(String text) {
ArrayList<String> itemlist = new ArrayList<>();

while (text != null) {
String[] subtexts = getText(text);
if (subtexts == null)
break;

//바깥 앞의 문자열은 큰따음표가 없는 데이터이다
String fronttext = subtexts[0];
if (fronttext != null && fronttext.length() > 1) {
//양끝의 콤마를 제거 한다
if (fronttext.charAt(0) == ',')
fronttext = fronttext.substring(1);

int lastindex = fronttext.length() - 1;
if (fronttext.charAt(lastindex) == ',')
fronttext = fronttext.substring(0, lastindex);

//콤마를 기준으로 나눠서 저장한다
String[] addtexts = fronttext.split(",");
for (int i = 0 ; i < addtexts.length ; i++)
itemlist.add(addtexts[i]);

//만약 마지막에 콤마가 있다면
//빈데이터가 하나 더 있는것이므로 따로 저장한다
if (fronttext.charAt(fronttext.length() - 1) == ',')
itemlist.add("");
}

//안의 문자열은 그대로 저장한다
String contentstext = subtexts[1];
if (contentstext != null)
itemlist.add(contentstext);

//바깥 뒤의 문자열은 계속 데이터를 추출할 수 있도록 해준다
text = subtexts[2];
}

return itemlist;
}



확인해 보자~


ArrayList<String> itemlist = getItemList("56,\"10,000\",\"1,500\",300,890,\"2,360,050\",704");

for (int i = 0 ; i < itemlist.size() ; i++)
Log.e("MyLog", "itemlist: " + itemlist.get(i));


itemlist: 56

itemlist: 10,000

itemlist: 1,500

itemlist: 300

itemlist: 890

itemlist: 2,360,050

itemlist: 704




 

 



수제 앱 장인: 고영진


고영진모바일 1인기업 대표 겸 개발자 

  

     실패만 하고 있어도 꿈을 포기하지 않는 남자 

     제가 직접 경험하고 습득한 지식을 위주로 올릴게요

 




사업자 정보 표시
고영진모바일 | 고영진 | 서울특별시 관악구 낙성대동 서울대연구공원 SK상생혁신센터 | 사업자 등록번호 : 109-11-82076 | TEL : 010-9990-3674 | Mail : gyjmeba@hanmail.net | 통신판매신고번호 : 2010-서울강서-0217호 | 사이버몰의 이용약관 바로가기
개발/android 2016.10.24 10:47

막대 그래프의 처음은 이렇게





가장 기본이 되는 그래프인 막대 그래프를 만들어 보자~


위와 같이 막대와 그 수치를 넣을 것이다

예전에 올렸던 꺽은선 그래프를 살짝 변경해서 코딩했다.



private Paint mLinePaint, mTextPaint;

private float mTextGap;
private int[] mPoints, mPointX, mPointY;
private int mUnit, mOrigin, mDivide;

public GraphView(Context context, AttributeSet attrs) {
super(context, attrs);

setTypes(context, attrs);
}

//그래프 옵션을 받는다
private void setTypes(Context context, AttributeSet attrs) {
TypedArray types = context.obtainStyledAttributes(attrs, R.styleable.GraphView);

//수치 옵션
Paint paint = new Paint();
paint.setColor(types.getColor(R.styleable.GraphView_textColor, Color.BLACK));
paint.setTextSize(types.getDimension(R.styleable.GraphView_textSize, 0));
paint.setTextAlign(Paint.Align.CENTER);
mTextPaint = paint;

//막대와 수치와의 거리
mTextGap = types.getDimension(R.styleable.GraphView_textGap, 0);

//막대 옵션
paint = new Paint();
paint.setColor(types.getColor(R.styleable.GraphView_lineColor, Color.BLACK));
paint.setStrokeWidth(types.getDimension(R.styleable.GraphView_lineThickness, 0));
mLinePaint = paint;
}

//그래프 정보를 받는다
public void setPoints(int[] points, int unit, int origin, int divide) {
mPoints = points; //y축 값 배열

mUnit = unit; //y축 단위
mOrigin = origin; //y축 원점
mDivide = divide; //y축 값 갯수
}

//그래프를 만든다
public void draw() {
int height = getHeight();
int[] points = mPoints;

//x축 막대 사이의 거리
float gapx = (float)getWidth() / points.length;

//y축 단위 사이의 거리
float gapy = height / mDivide;

float halfgab = gapx / 2;

int length = points.length;
mPointX = new int[length];
mPointY = new int[length];

for(int i = 0 ; i < length ; i++) {
//막대 좌표를 구한다
int x = (int)(halfgab + (i * gapx));
int y = (int)(height - (((points[i] / mUnit) - mOrigin) * gapy));

mPointX[i] = x;
mPointY[i] = y;
}
}

//그래프를 그린다(onCreate 등에서 호출시)
public void drawForBeforeDrawView() {
//뷰의 크기를 계산하여 그래프를 그리기 때문에 뷰가 실제로 만들어진 시점에서 함수를 호출해 준다
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
draw();

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
getViewTreeObserver().removeGlobalOnLayoutListener(this);
else
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

if(mPointX != null && mPointY != null) {
int length = mPointX.length;

int bottom = getHeight();
for (int i = 0; i < length; i++) {
int x = mPointX[i];
int y = mPointY[i];

//믹대 위 수치를 쓴다
canvas.drawText("" + mPoints[i], x, y - mTextGap, mTextPaint);

//믹대를 그린다
canvas.drawLine(x, y, x, bottom, mLinePaint);
}
}
}



옵션 attrs.xml 이다


<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="GraphView">
<attr name="textColor" format="color"/>
<attr name="textSize" format="dimension"/>
<attr name="textGap" format="dimension"/>

<attr name="lineColor" format="color"/>
<attr name="lineThickness" format="dimension"/>
</declare-styleable>
</resources>



layout은 이렇다


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">

<kr.gyjmoble.graphtest.GraphView android:id="@+id/GraphView"
xmlns:gyjmobile="http://schemas.android.com/apk/res/kr.gyjmoble.graphtest"
android:layout_width="match_parent" android:layout_height="match_parent"
gyjmobile:textColor="#ec4800" gyjmobile:textSize="15sp"
gyjmobile:textGap="2dp"
gyjmobile:lineColor="#0073cc" gyjmobile:lineThickness="20dp"/>

</LinearLayout>



자 이젠 사용하자


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

//그래프에 들어갈 수치 배열
int[] points = {5, 3, 7, 8, 4, 3, 3, 6, 4, 1};

GraphView graphview = (GraphView) findViewById(R.id.GraphView);

//단위는 1씩, 원점은 0, 총 10줄로 나누어진 그래프를 그린다
graphview.setPoints(points, 1, 0, 10);
graphview.drawForBeforeDrawView();
}



처음은 이렇게 간단하게 출발해서 다양한 그래프를 만들면 된다~




 

 



수제 앱 장인: 고영진


고영진모바일 1인기업 대표 겸 개발자 

  

     실패만 하고 있어도 꿈을 포기하지 않는 남자 

     제가 직접 경험하고 습득한 지식을 위주로 올릴게요

 




사업자 정보 표시
고영진모바일 | 고영진 | 서울특별시 관악구 낙성대동 서울대연구공원 SK상생혁신센터 | 사업자 등록번호 : 109-11-82076 | TEL : 010-9990-3674 | Mail : gyjmeba@hanmail.net | 통신판매신고번호 : 2010-서울강서-0217호 | 사이버몰의 이용약관 바로가기
개발/android 2016.09.26 11:24

재귀 함수의 완벽한 이해

재귀 함수란 자기 자신을 다시 호출하는 함수를 말한다.

하지만 이러한 개념을 알고 있어도 직접 구현할려고 하면 헷갈린다.


특히, 분기를 위한 반복문이 함수내에 들어간다면 더더욱 그렇다.


거리 계산 다이어그램을 예로 해서 살펴보자





위 다이어그램의 숫자들은 각 점끼리의 거리이다.

즉, 총 거리는 숫자들을 모두 더한 13이고,

D까지는 3 + 2 + 4 = 9가 된다.


재귀 함수로 코드화 해보자 


private static int NAME_A = 0;
private static int NAME_B = 1;
private static int NAME_C = 2;
private static int NAME_D = 3;
private static int NAME_E = 4;
private static int NAME_F = 5;
private static int NAME_G = 6;
private static int NAME_H = 7;


private int getDistance(int[] points, int index, int searchindex, int distance) {
//거리 누적
distance += points[index];
Log.d("MyLog", "getDistance: " + distance);

//목적지까지 도착하면 멈춤
if(index == searchindex)
return distance;

//다음 점으로 재귀 호출
return getDistance(points, ++index, searchindex, distance);
}


//점의 거리 배열
int[] points = {0, 3, 2, 4, 3, 1};

//D까지의 거리 계산
int distance = getDistance(points, NAME_A, NAME_D, 0);

Log.d("MyLog", "Distance: " + distance);


결과 :


D/MyLog: getDistance: 0

D/MyLog: getDistance: 3

D/MyLog: getDistance: 5

D/MyLog: getDistance: 9

D/MyLog: Distance: 9




여기까지는 원래 반복문 하나로도 구현 가능하다

하지만 중간에 분기가 포함되면 재귀 함수의 면모가 드러난다



위 다이어그램은 C에서 두갈래로 갈라진다

코드로는 재귀 함수에 반복문을 추가해준다


//점 거리 클래스
private class Point {
public Point[] mNextPoint;
public int mDistance;

public Point(int distance, Point[] nextpoint) {
mDistance = distance;
mNextPoint = nextpoint;
}
}


private int getDistance(Point point, Point searchpoint, int distance) {
while (true) {
//거리 누적
distance += point.mDistance;
Log.d("MyLog", "getDistance: " + distance);

//목적지까지 도착하면 멈춤
if (point == searchpoint)
return distance;

//다음 점 찾기
Point[] nextpoints = point.mNextPoint;
if(nextpoints == null) {
//더이상 길이 없으면 종료
break;
}
else {
for (int i = 0; i < nextpoints.length; i++) {
if(i == 0) {
//첫번째 길은 계속 반복문을 돌림
point = nextpoints[i];
}
else {
//두번째 길부터는 재귀 호출
int result = getDistance(nextpoints[i], searchpoint, distance);

//목적지를 찾았을때에만 누적 거리 반환
//그렇지 않으면 이어서 반복문을 돌림
if(result >= 0)
return result;
}
}
}
}

return -1;
}


//점의 거리 배열
Point[] points = new Point[8];
points[NAME_H] = new Point(3, null);
points[NAME_G] = new Point(2, new Point[] {points[NAME_H]});
points[NAME_F] = new Point(1, null);
points[NAME_E] = new Point(3, new Point[] {points[NAME_F]});
points[NAME_D] = new Point(4, new Point[] {points[NAME_E]});
points[NAME_C] = new Point(2, new Point[] {points[NAME_D],points[NAME_G]});
points[NAME_B] = new Point(3, new Point[] {points[NAME_C]});
points[NAME_A] = new Point(0, new Point[] {points[NAME_B]});

//A에서 G까지의 거리 계산
int distance = getDistance(points[NAME_A], points[NAME_G], 0);
Log.d("MyLog", "Distance G: " + distance);

//A에서 E까지의 거리 계산
distance = getDistance(points[NAME_A], points[NAME_E], 0);
Log.d("MyLog", "Distance E: " + distance);


결과 : 


D/MyLog: getDistance: 0

D/MyLog: getDistance: 3

D/MyLog: getDistance: 5

D/MyLog: getDistance: 7

D/MyLog: Distance G: 7


D/MyLog: getDistance: 0

D/MyLog: getDistance: 3

D/MyLog: getDistance: 5

D/MyLog: getDistance: 7

D/MyLog: getDistance: 10

D/MyLog: getDistance: 9

D/MyLog: getDistance: 12

D/MyLog: Distance E: 12




재귀 함수의 핵심은 반환값이다.


본류와 지류를 구분해 놓고

본류는 반복문이 계속 되게 놔두고 지류만 재귀 호출을 한다.

그리고 성공인지 실패인지 반환값을 받아 계속할지 끝낼지 

결정하면 된다. 



 

 



수제 앱 장인: 고영진


고영진모바일 1인기업 대표 겸 개발자 

  

     실패만 하고 있어도 꿈을 포기하지 않는 남자 

     제가 직접 경험하고 습득한 지식을 위주로 올릴게요

 




사업자 정보 표시
고영진모바일 | 고영진 | 서울특별시 관악구 낙성대동 서울대연구공원 SK상생혁신센터 | 사업자 등록번호 : 109-11-82076 | TEL : 010-9990-3674 | Mail : gyjmeba@hanmail.net | 통신판매신고번호 : 2010-서울강서-0217호 | 사이버몰의 이용약관 바로가기
개발/android 2016.07.18 10:33

TimePicker, DatePicker, NumberPicker의 폰트 바꾸기



시간을 쉽게 선택할 수 있는 TimePicker,

날짜를 쉽게 선택할 수 있는 DatePicker


이것들은 NumberPicker로 이루어진 UI이다.

그래서 이 Picker들의 폰트를 바꾸기 위해서는  NumberPicker의 구조를 알아야 한다.


NumberPicker는 현재 선택된 숫자를 보여주는 TextView와 

그 주위의 숫자를 스크롤하며 보여주는 휠 뷰로 구성된다.

(스타일을 Theme.Holo.Light 로 적용했을 경우)


즉, 폰트를 바꿔줄때 이 두가지를 모두 바꿔줘야 한다는 의미다.


public void setTypeface(NumberPicker picker, Typeface typeface) {
//주위의 숫자들을 보여주는 휠 뷰
try {
//클래스의 휠 Paint 객체를 꺼내 폰트를 적용
Field field = NumberPicker.class.getDeclaredField("mSelectorWheelPaint");
field.setAccessible(true);
((Paint)field.get(picker)).setTypeface(typeface);

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}

//선택된 숫자를 보여주는 TextView
int count = picker.getChildCount();
for(int i = 0 ; i < count ; i++) {
//자식뷰로 꺼내 폰트를 적용
View view = picker.getChildAt(i);
if (view instanceof TextView)
((TextView)view).setTypeface(typeface);
}
}



이를 TimePicker에 적용하면 이렇다.

DatePicker도 같은 방식으로 하면 된다. 


public class MyTimePicker extends TimePicker {

public MyTimePicker(Context context) {
super(context);
}

public MyTimePicker(Context context, AttributeSet attrs) {
super(context, attrs);
}

public void setTypeface(Typeface typeface) {
LinearLayout layout = (LinearLayout)getChildAt(0);

for(int i = 0 ; i < layout.getChildCount() ; i++) {
LinearLayout layout1 = (LinearLayout) layout.getChildAt(i);

//NumberPicker가 있는 자식뷰에서 적용
for(int j = 0 ; j < layout1.getChildCount() ; j++) {
View view = layout1.getChildAt(j);
if (view instanceof NumberPicker)
setTypeface((NumberPicker)view, typeface);
}
}
}
}


 

 



수제 앱 장인: 고영진


고영진모바일 1인기업 대표 겸 개발자 

  

     실패만 하고 있어도 꿈을 포기하지 않는 남자 

     제가 직접 경험하고 습득한 지식을 위주로 올릴게요

 




사업자 정보 표시
고영진모바일 | 고영진 | 서울특별시 관악구 낙성대동 서울대연구공원 SK상생혁신센터 | 사업자 등록번호 : 109-11-82076 | TEL : 010-9990-3674 | Mail : gyjmeba@hanmail.net | 통신판매신고번호 : 2010-서울강서-0217호 | 사이버몰의 이용약관 바로가기
개발/android 2016.05.30 10:59

카메라 위에 스킨을 넣어 찍어보자~



카메라 사진 위에 그림이나 글자를 넣어 찍고 싶다면??

의외로 간단하다... 아래를 보시라~




사진을 찍을려면 먼저 카메라를 연동해야 한다.

이부분은 비록 복잡해 보이지만 어차피 안드로이드 샘플 예제에 다 나오는 내용이다.

그런데 룰리팝 이후로 카메라 연동방법이 바뀌는 바람에 다시 삽질좀 하며 작성했다ㅎㅎ 


이글의 주제는 카메라 연동이 아니니 참고만... 설명도 생략...



private CameraManager mCameraManager;
private CameraDevice mCameraDevice;
private Camera mCamera;
private Size mPreviewSize;
private CaptureRequest.Builder mPreviewBuilder, mPictureBuilder;
private CameraCaptureSession mCaptureSession;
private ImageReader mImageReader;


private void createTextureView() {

    TextureView view = findViewById(R.id.TextureView);


    mCameraManager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);


    view.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {

        @Override
       
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {

            openCamera();

        }


        @Override
       
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

            

        }


        @Override
       
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {

            closeCamera();

            return false;

        }


        @Override
       
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

           
       
}

    });

}



private void openCamera() {

    try {

        String[] ids = mCameraManager.getCameraIdList();


        int length = ids.length;

        for (int i = 0; i < length; i++) {


            CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(ids[i]);

            if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK) {

                StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);


                mPreviewSize = map.getOutputSizes(SurfaceTexture.class)[i];

                Size size = map.getOutputSizes(ImageFormat.JPEG)[i];


                ImageReader reader = ImageReader.newInstance(size.getWidth(), size.getHeight(), ImageFormat.JPEG, 1);

                reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {


                    @Override
                   
public void onImageAvailable(ImageReader reader) {                      

                        ByteBuffer buffer = reader.acquireLatestImage().getPlanes()[0].getBuffer();

                        byte[] bytes = new byte[buffer.capacity()];

                        buffer.get(bytes);


//여기서 사진데이터를 받아 이미지를 만듬

                        makeImage(bytes);

                    }


                }, mPictureHandler);


                mImageReader = reader;


                mCameraManager.openCamera(ids[i], mCameraStateCallback, null);

            }

        }


    } catch (CameraAccessException e) {

        e.printStackTrace();

    }

}



private CameraDevice.StateCallback mCameraStateCallback = new CameraDevice.StateCallback() {
    @Override
   
public void onOpened(CameraDevice camera) {
        mCameraDevice = camera;
        try {
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
            Surface surface = new Surface(texture);

            CaptureRequest.Builder builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            builder.addTarget(surface);
            mPreviewBuilder = builder;

            camera.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() {

                @Override
               
public void onConfigured(CameraCaptureSession session) {
                    mCaptureSession = session;
                    setPreview(session);
                }

                @Override
               
public void onConfigureFailed(CameraCaptureSession session) {
     
                }
            }, null);

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

    }

    @Override
   
public void onDisconnected(CameraDevice camera) {

    }

    @Override
   
public void onError(CameraDevice camera, int error) {

    }
};


private void closeCamera() {
   
if (mCaptureSession != null) {
        mCaptureSession.close();
        mCaptureSession = null;
    }

    if (mCameraDevice != null) {
        mCameraDevice.close();
        mCameraDevice = null;
    }

    if (mImageReader != null) {
        mImageReader.close();
        mImageReader = null;
    }
}


private void setPreview(CameraCaptureSession session) {
    mPreviewBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);

    HandlerThread thread = new HandlerThread("CameraPreview");
    thread.start();
    Handler backgroundHandler = new Handler(thread.getLooper());

    try {
        session.setRepeatingRequest(mPreviewBuilder.build(), null, backgroundHandler);
    } catch (CameraAccessException e) {

        e.printStackTrace();
    }
}






찍은 사진데이터를 비트맵으로 변경하면서 그 위에 스킨을 넣는 부분이다.

그냥 스킨이 되는 레이아웃뷰들의 캡쳐이미지를 가져와 사진이미지에 덮어주기만 하면된다.

이 기본원리로 다양한 응용이 가능할 것이다...



private Bitmap makeImage(byte[] datas) {

    //카메라 사진데이터를 이용해 비트맵이미지를 만든다 
   
Bitmap cameraimage;
    try {
        cameraimage = BitmapFactory.decodeByteArray(datas, 0, datas.length);

    } catch (OutOfMemoryError e) {
        //보통 사진데이터가 크기 때문에 메모리오류가 나면 크기를 줄인다
       
BitmapFactory.Options options = new BitmapFactory.Options();
        options.inSampleSize = 2;

        cameraimage = BitmapFactory.decodeByteArray(datas, 0, datas.length, options);
    }

    if (cameraimage != null) {
        //이미지나 글자가 포함된 레이아웃의 갭쳐이미지를 불려온다
       
View view = findViewById(R.id.SkinLayout);
        view.setDrawingCacheEnabled(true);
        Bitmap skinimage = view.getDrawingCache();

        //세로모드일때는 카메라 사진을 회전시켜준다(기종에 따라 다를수 있다)
       
Matrix matrix = new Matrix();
        matrix.setRotate(90);
        cameraimage = Bitmap.createBitmap(cameraimage, 0, 0, cameraimage.getWidth(), cameraimage.getHeight(), matrix, false);
       
        //카메라 사진위에 레이아웃 갭쳐이미지를 덮어 그린다
       
Canvas canvas = new Canvas(cameraimage);
        canvas.drawBitmap(skinimage, 0, 0, null);
        skinimage.recycle();


        return cameraimage;

    }


    return null;
}






XML은 카메라뷰 위에 스킨레이아웃을 겹친다




<?xml version="1.0" encoding="utf-8"?>
<
RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">

    <
TextureView android:id="@+id/TextureView"
       
android:layout_width="match_parent" android:layout_height="match_parent"/>

    <
LinearLayout android:id="@+id/SkinLayout"
       
android:layout_width="match_parent" android:layout_height="match_parent">

       
<!-- 여기에 이미지나 글자를 -->

   
</LinearLayout>

</
RelativeLayout>



 

 



수제 앱 장인: 고영진


고영진모바일 1인기업 대표 겸 개발자 

  

     실패만 하고 있어도 꿈을 포기하지 않는 남자 

     제가 직접 경험하고 습득한 지식을 위주로 올릴게요

 




사업자 정보 표시
고영진모바일 | 고영진 | 서울특별시 관악구 낙성대동 서울대연구공원 SK상생혁신센터 | 사업자 등록번호 : 109-11-82076 | TEL : 010-9990-3674 | Mail : gyjmeba@hanmail.net | 통신판매신고번호 : 2010-서울강서-0217호 | 사이버몰의 이용약관 바로가기
TOTAL 69,579 TODAY 8